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 create_workspace(
45 &self,
46 name: String,
47 description: Option<String>,
48 owner_id: Uuid,
49 ) -> Result<TeamWorkspace> {
50 let mut workspace = TeamWorkspace::new(name.clone(), owner_id);
51 workspace.description = description.clone();
52
53 if let Some(core_bridge) = &self.core_bridge {
55 let core_workspace = core_bridge.create_empty_workspace(name, owner_id)?;
56 workspace.config = core_workspace.config;
57 } else {
58 workspace.config = serde_json::json!({
60 "name": workspace.name,
61 "description": workspace.description,
62 "folders": [],
63 "requests": []
64 });
65 }
66
67 sqlx::query!(
69 r#"
70 INSERT INTO workspaces (id, name, description, owner_id, config, version, created_at, updated_at, is_archived)
71 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
72 "#,
73 workspace.id,
74 workspace.name,
75 workspace.description,
76 workspace.owner_id,
77 workspace.config,
78 workspace.version,
79 workspace.created_at,
80 workspace.updated_at,
81 workspace.is_archived
82 )
83 .execute(&self.db)
84 .await?;
85
86 let member = WorkspaceMember::new(workspace.id, owner_id, UserRole::Admin);
88 sqlx::query!(
89 r#"
90 INSERT INTO workspace_members (id, workspace_id, user_id, role, joined_at, last_activity)
91 VALUES (?, ?, ?, ?, ?, ?)
92 "#,
93 member.id,
94 member.workspace_id,
95 member.user_id,
96 member.role,
97 member.joined_at,
98 member.last_activity
99 )
100 .execute(&self.db)
101 .await?;
102
103 self.cache.write().insert(workspace.id, workspace.clone());
105
106 Ok(workspace)
107 }
108
109 pub async fn get_workspace(&self, workspace_id: Uuid) -> Result<TeamWorkspace> {
111 if let Some(workspace) = self.cache.read().get(&workspace_id) {
113 return Ok(workspace.clone());
114 }
115
116 let workspace = sqlx::query_as!(
118 TeamWorkspace,
119 r#"
120 SELECT
121 id as "id: Uuid",
122 name,
123 description,
124 owner_id as "owner_id: Uuid",
125 config,
126 version,
127 created_at as "created_at: chrono::DateTime<chrono::Utc>",
128 updated_at as "updated_at: chrono::DateTime<chrono::Utc>",
129 is_archived as "is_archived: bool"
130 FROM workspaces
131 WHERE id = ?
132 "#,
133 workspace_id
134 )
135 .fetch_optional(&self.db)
136 .await?
137 .ok_or_else(|| CollabError::WorkspaceNotFound(workspace_id.to_string()))?;
138
139 self.cache.write().insert(workspace_id, workspace.clone());
141
142 Ok(workspace)
143 }
144
145 pub async fn update_workspace(
147 &self,
148 workspace_id: Uuid,
149 user_id: Uuid,
150 name: Option<String>,
151 description: Option<String>,
152 config: Option<serde_json::Value>,
153 ) -> Result<TeamWorkspace> {
154 let member = self.get_member(workspace_id, user_id).await?;
156 PermissionChecker::check(member.role, Permission::WorkspaceUpdate)?;
157
158 let mut workspace = self.get_workspace(workspace_id).await?;
159
160 if let Some(name) = name {
162 workspace.name = name;
163 }
164 if let Some(description) = description {
165 workspace.description = Some(description);
166 }
167 if let Some(config) = config {
168 workspace.config = config;
169 }
170 workspace.updated_at = Utc::now();
171 workspace.version += 1;
172
173 sqlx::query!(
175 r#"
176 UPDATE workspaces
177 SET name = ?, description = ?, config = ?, version = ?, updated_at = ?
178 WHERE id = ?
179 "#,
180 workspace.name,
181 workspace.description,
182 workspace.config,
183 workspace.version,
184 workspace.updated_at,
185 workspace.id
186 )
187 .execute(&self.db)
188 .await?;
189
190 self.cache.write().insert(workspace_id, workspace.clone());
192
193 Ok(workspace)
194 }
195
196 pub async fn delete_workspace(&self, workspace_id: Uuid, user_id: Uuid) -> Result<()> {
198 let member = self.get_member(workspace_id, user_id).await?;
200 PermissionChecker::check(member.role, Permission::WorkspaceDelete)?;
201
202 let now = Utc::now();
203 sqlx::query!(
204 r#"
205 UPDATE workspaces
206 SET is_archived = TRUE, updated_at = ?
207 WHERE id = ?
208 "#,
209 now,
210 workspace_id
211 )
212 .execute(&self.db)
213 .await?;
214
215 self.cache.write().remove(&workspace_id);
217
218 Ok(())
219 }
220
221 pub async fn add_member(
223 &self,
224 workspace_id: Uuid,
225 user_id: Uuid,
226 new_member_id: Uuid,
227 role: UserRole,
228 ) -> Result<WorkspaceMember> {
229 let member = self.get_member(workspace_id, user_id).await?;
231 PermissionChecker::check(member.role, Permission::InviteMembers)?;
232
233 let new_member = WorkspaceMember::new(workspace_id, new_member_id, role);
235
236 sqlx::query!(
237 r#"
238 INSERT INTO workspace_members (id, workspace_id, user_id, role, joined_at, last_activity)
239 VALUES (?, ?, ?, ?, ?, ?)
240 "#,
241 new_member.id,
242 new_member.workspace_id,
243 new_member.user_id,
244 new_member.role,
245 new_member.joined_at,
246 new_member.last_activity
247 )
248 .execute(&self.db)
249 .await?;
250
251 Ok(new_member)
252 }
253
254 pub async fn remove_member(
256 &self,
257 workspace_id: Uuid,
258 user_id: Uuid,
259 member_to_remove: Uuid,
260 ) -> Result<()> {
261 let member = self.get_member(workspace_id, user_id).await?;
263 PermissionChecker::check(member.role, Permission::RemoveMembers)?;
264
265 let workspace = self.get_workspace(workspace_id).await?;
267 if member_to_remove == workspace.owner_id {
268 return Err(CollabError::InvalidInput("Cannot remove workspace owner".to_string()));
269 }
270
271 sqlx::query!(
272 r#"
273 DELETE FROM workspace_members
274 WHERE workspace_id = ? AND user_id = ?
275 "#,
276 workspace_id,
277 member_to_remove
278 )
279 .execute(&self.db)
280 .await?;
281
282 Ok(())
283 }
284
285 pub async fn change_role(
287 &self,
288 workspace_id: Uuid,
289 user_id: Uuid,
290 member_id: Uuid,
291 new_role: UserRole,
292 ) -> Result<WorkspaceMember> {
293 let member = self.get_member(workspace_id, user_id).await?;
295 PermissionChecker::check(member.role, Permission::ChangeRoles)?;
296
297 let workspace = self.get_workspace(workspace_id).await?;
299 if member_id == workspace.owner_id {
300 return Err(CollabError::InvalidInput(
301 "Cannot change workspace owner's role".to_string(),
302 ));
303 }
304
305 sqlx::query!(
306 r#"
307 UPDATE workspace_members
308 SET role = ?
309 WHERE workspace_id = ? AND user_id = ?
310 "#,
311 new_role,
312 workspace_id,
313 member_id
314 )
315 .execute(&self.db)
316 .await?;
317
318 self.get_member(workspace_id, member_id).await
319 }
320
321 pub async fn get_member(&self, workspace_id: Uuid, user_id: Uuid) -> Result<WorkspaceMember> {
323 sqlx::query_as!(
324 WorkspaceMember,
325 r#"
326 SELECT
327 id as "id: Uuid",
328 workspace_id as "workspace_id: Uuid",
329 user_id as "user_id: Uuid",
330 role as "role: UserRole",
331 joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
332 last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
333 FROM workspace_members
334 WHERE workspace_id = ? AND user_id = ?
335 "#,
336 workspace_id,
337 user_id
338 )
339 .fetch_optional(&self.db)
340 .await?
341 .ok_or_else(|| CollabError::AuthorizationFailed("User is not a member".to_string()))
342 }
343
344 pub async fn list_members(&self, workspace_id: Uuid) -> Result<Vec<WorkspaceMember>> {
346 let members = sqlx::query_as!(
347 WorkspaceMember,
348 r#"
349 SELECT
350 id as "id: Uuid",
351 workspace_id as "workspace_id: Uuid",
352 user_id as "user_id: Uuid",
353 role as "role: UserRole",
354 joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
355 last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
356 FROM workspace_members
357 WHERE workspace_id = ?
358 ORDER BY joined_at
359 "#,
360 workspace_id
361 )
362 .fetch_all(&self.db)
363 .await?;
364
365 Ok(members)
366 }
367
368 pub async fn list_user_workspaces(&self, user_id: Uuid) -> Result<Vec<TeamWorkspace>> {
370 let workspaces = sqlx::query_as!(
371 TeamWorkspace,
372 r#"
373 SELECT
374 w.id as "id: Uuid",
375 w.name,
376 w.description,
377 w.owner_id as "owner_id: Uuid",
378 w.config,
379 w.version,
380 w.created_at as "created_at: chrono::DateTime<chrono::Utc>",
381 w.updated_at as "updated_at: chrono::DateTime<chrono::Utc>",
382 w.is_archived as "is_archived: bool"
383 FROM workspaces w
384 INNER JOIN workspace_members m ON w.id = m.workspace_id
385 WHERE m.user_id = ? AND w.is_archived = FALSE
386 ORDER BY w.updated_at DESC
387 "#,
388 user_id
389 )
390 .fetch_all(&self.db)
391 .await?;
392
393 Ok(workspaces)
394 }
395
396 pub async fn fork_workspace(
401 &self,
402 source_workspace_id: Uuid,
403 new_name: Option<String>,
404 new_owner_id: Uuid,
405 fork_point_commit_id: Option<Uuid>,
406 ) -> Result<TeamWorkspace> {
407 self.get_member(source_workspace_id, new_owner_id).await?;
409
410 let source_workspace = self.get_workspace(source_workspace_id).await?;
412
413 let mut forked_workspace = TeamWorkspace::new(
415 new_name.unwrap_or_else(|| format!("{} (Fork)", source_workspace.name)),
416 new_owner_id,
417 );
418 forked_workspace.description = source_workspace.description.clone();
419
420 if let Some(core_bridge) = &self.core_bridge {
423 if let Ok(mut core_workspace) = core_bridge.team_to_core(&source_workspace) {
425 core_workspace.id = forked_workspace.id.to_string();
427 core_workspace.name = forked_workspace.name.clone();
428 core_workspace.description = forked_workspace.description.clone();
429 core_workspace.created_at = forked_workspace.created_at;
430 core_workspace.updated_at = forked_workspace.updated_at;
431
432 Self::regenerate_entity_ids(&mut core_workspace);
434
435 if let Ok(team_ws) = core_bridge.core_to_team(&core_workspace, new_owner_id) {
437 forked_workspace.config = team_ws.config;
438 } else {
439 forked_workspace.config = source_workspace.config.clone();
441 }
442 } else {
443 forked_workspace.config = source_workspace.config.clone();
445 }
446 } else {
447 forked_workspace.config = source_workspace.config.clone();
449 }
450
451 sqlx::query!(
453 r#"
454 INSERT INTO workspaces (id, name, description, owner_id, config, version, created_at, updated_at, is_archived)
455 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
456 "#,
457 forked_workspace.id,
458 forked_workspace.name,
459 forked_workspace.description,
460 forked_workspace.owner_id,
461 forked_workspace.config,
462 forked_workspace.version,
463 forked_workspace.created_at,
464 forked_workspace.updated_at,
465 forked_workspace.is_archived
466 )
467 .execute(&self.db)
468 .await?;
469
470 let member = WorkspaceMember::new(forked_workspace.id, new_owner_id, UserRole::Admin);
472 sqlx::query!(
473 r#"
474 INSERT INTO workspace_members (id, workspace_id, user_id, role, joined_at, last_activity)
475 VALUES (?, ?, ?, ?, ?, ?)
476 "#,
477 member.id,
478 member.workspace_id,
479 member.user_id,
480 member.role,
481 member.joined_at,
482 member.last_activity
483 )
484 .execute(&self.db)
485 .await?;
486
487 let fork = WorkspaceFork::new(
489 source_workspace_id,
490 forked_workspace.id,
491 new_owner_id,
492 fork_point_commit_id,
493 );
494 sqlx::query!(
495 r#"
496 INSERT INTO workspace_forks (id, source_workspace_id, forked_workspace_id, forked_at, forked_by, fork_point_commit_id)
497 VALUES (?, ?, ?, ?, ?, ?)
498 "#,
499 fork.id,
500 fork.source_workspace_id,
501 fork.forked_workspace_id,
502 fork.forked_at,
503 fork.forked_by,
504 fork.fork_point_commit_id
505 )
506 .execute(&self.db)
507 .await?;
508
509 self.cache.write().insert(forked_workspace.id, forked_workspace.clone());
511
512 Ok(forked_workspace)
513 }
514
515 pub async fn list_forks(&self, workspace_id: Uuid) -> Result<Vec<WorkspaceFork>> {
517 let forks = sqlx::query_as!(
518 WorkspaceFork,
519 r#"
520 SELECT
521 id as "id: Uuid",
522 source_workspace_id as "source_workspace_id: Uuid",
523 forked_workspace_id as "forked_workspace_id: Uuid",
524 forked_at as "forked_at: chrono::DateTime<chrono::Utc>",
525 forked_by as "forked_by: Uuid",
526 fork_point_commit_id as "fork_point_commit_id: Uuid"
527 FROM workspace_forks
528 WHERE source_workspace_id = ?
529 ORDER BY forked_at DESC
530 "#,
531 workspace_id
532 )
533 .fetch_all(&self.db)
534 .await?;
535
536 Ok(forks)
537 }
538
539 pub async fn get_fork_source(
541 &self,
542 forked_workspace_id: Uuid,
543 ) -> Result<Option<WorkspaceFork>> {
544 let fork = sqlx::query_as!(
545 WorkspaceFork,
546 r#"
547 SELECT
548 id as "id: Uuid",
549 source_workspace_id as "source_workspace_id: Uuid",
550 forked_workspace_id as "forked_workspace_id: Uuid",
551 forked_at as "forked_at: chrono::DateTime<chrono::Utc>",
552 forked_by as "forked_by: Uuid",
553 fork_point_commit_id as "fork_point_commit_id: Uuid"
554 FROM workspace_forks
555 WHERE forked_workspace_id = ?
556 "#,
557 forked_workspace_id
558 )
559 .fetch_optional(&self.db)
560 .await?;
561
562 Ok(fork)
563 }
564
565 fn regenerate_entity_ids(core_workspace: &mut mockforge_core::workspace::Workspace) {
567 use mockforge_core::workspace::{Folder, MockRequest};
568 use uuid::Uuid;
569
570 core_workspace.id = Uuid::new_v4().to_string();
572
573 fn regenerate_folder_ids(folder: &mut Folder) {
575 folder.id = Uuid::new_v4().to_string();
576 for subfolder in &mut folder.folders {
577 regenerate_folder_ids(subfolder);
578 }
579 for request in &mut folder.requests {
580 request.id = Uuid::new_v4().to_string();
581 }
582 }
583
584 for folder in &mut core_workspace.folders {
586 regenerate_folder_ids(folder);
587 }
588
589 for request in &mut core_workspace.requests {
591 request.id = Uuid::new_v4().to_string();
592 }
593 }
594}
595
596pub struct WorkspaceManager {
598 service: Arc<WorkspaceService>,
599}
600
601impl WorkspaceManager {
602 pub fn new(service: Arc<WorkspaceService>) -> Self {
604 Self { service }
605 }
606
607 pub async fn create_workspace(
609 &self,
610 name: String,
611 description: Option<String>,
612 owner_id: Uuid,
613 ) -> Result<TeamWorkspace> {
614 self.service.create_workspace(name, description, owner_id).await
615 }
616
617 pub async fn get_workspace(&self, workspace_id: Uuid, user_id: Uuid) -> Result<TeamWorkspace> {
619 self.service.get_member(workspace_id, user_id).await?;
621 self.service.get_workspace(workspace_id).await
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628
629 }