use crate::core_bridge::CoreBridge;
use crate::error::{CollabError, Result};
use crate::models::{TeamWorkspace, UserRole, WorkspaceFork, WorkspaceMember};
use crate::permissions::{Permission, PermissionChecker};
use chrono::Utc;
use parking_lot::RwLock;
use sqlx::{Pool, Sqlite};
use std::collections::HashMap;
use std::sync::Arc;
use uuid::Uuid;
pub struct WorkspaceService {
db: Pool<Sqlite>,
cache: Arc<RwLock<HashMap<Uuid, TeamWorkspace>>>,
core_bridge: Option<Arc<CoreBridge>>,
}
impl WorkspaceService {
#[must_use]
pub fn new(db: Pool<Sqlite>) -> Self {
Self {
db,
cache: Arc::new(RwLock::new(HashMap::new())),
core_bridge: None,
}
}
#[must_use]
pub fn with_core_bridge(db: Pool<Sqlite>, core_bridge: Arc<CoreBridge>) -> Self {
Self {
db,
cache: Arc::new(RwLock::new(HashMap::new())),
core_bridge: Some(core_bridge),
}
}
pub async fn check_database_health(&self) -> bool {
match sqlx::query("SELECT 1").execute(&self.db).await {
Ok(_) => true,
Err(e) => {
tracing::error!("Database health check failed: {}", e);
false
}
}
}
pub async fn create_workspace(
&self,
name: String,
description: Option<String>,
owner_id: Uuid,
) -> Result<TeamWorkspace> {
let mut workspace = TeamWorkspace::new(name.clone(), owner_id);
workspace.description.clone_from(&description);
if let Some(core_bridge) = &self.core_bridge {
let core_workspace = core_bridge.create_empty_workspace(name, owner_id)?;
workspace.config = core_workspace.config;
} else {
workspace.config = serde_json::json!({
"name": workspace.name,
"description": workspace.description,
"folders": [],
"requests": []
});
}
sqlx::query!(
r#"
INSERT INTO workspaces (id, name, description, owner_id, config, version, created_at, updated_at, is_archived)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
"#,
workspace.id,
workspace.name,
workspace.description,
workspace.owner_id,
workspace.config,
workspace.version,
workspace.created_at,
workspace.updated_at,
workspace.is_archived
)
.execute(&self.db)
.await?;
let member = WorkspaceMember::new(workspace.id, owner_id, UserRole::Admin);
sqlx::query!(
r#"
INSERT INTO workspace_members (id, workspace_id, user_id, role, joined_at, last_activity)
VALUES (?, ?, ?, ?, ?, ?)
"#,
member.id,
member.workspace_id,
member.user_id,
member.role,
member.joined_at,
member.last_activity
)
.execute(&self.db)
.await?;
self.cache.write().insert(workspace.id, workspace.clone());
Ok(workspace)
}
pub async fn get_workspace(&self, workspace_id: Uuid) -> Result<TeamWorkspace> {
if let Some(workspace) = self.cache.read().get(&workspace_id) {
return Ok(workspace.clone());
}
let workspace = sqlx::query_as!(
TeamWorkspace,
r#"
SELECT
id as "id: Uuid",
name,
description,
owner_id as "owner_id: Uuid",
config,
version,
created_at as "created_at: chrono::DateTime<chrono::Utc>",
updated_at as "updated_at: chrono::DateTime<chrono::Utc>",
is_archived as "is_archived: bool"
FROM workspaces
WHERE id = ?
"#,
workspace_id
)
.fetch_optional(&self.db)
.await?
.ok_or_else(|| CollabError::WorkspaceNotFound(workspace_id.to_string()))?;
self.cache.write().insert(workspace_id, workspace.clone());
Ok(workspace)
}
pub async fn update_workspace(
&self,
workspace_id: Uuid,
user_id: Uuid,
name: Option<String>,
description: Option<String>,
config: Option<serde_json::Value>,
) -> Result<TeamWorkspace> {
let member = self.get_member(workspace_id, user_id).await?;
PermissionChecker::check(member.role, Permission::WorkspaceUpdate)?;
let mut workspace = self.get_workspace(workspace_id).await?;
if let Some(name) = name {
workspace.name = name;
}
if let Some(description) = description {
workspace.description = Some(description);
}
if let Some(config) = config {
workspace.config = config;
}
workspace.updated_at = Utc::now();
workspace.version += 1;
sqlx::query!(
r#"
UPDATE workspaces
SET name = ?, description = ?, config = ?, version = ?, updated_at = ?
WHERE id = ?
"#,
workspace.name,
workspace.description,
workspace.config,
workspace.version,
workspace.updated_at,
workspace.id
)
.execute(&self.db)
.await?;
self.cache.write().insert(workspace_id, workspace.clone());
Ok(workspace)
}
pub async fn delete_workspace(&self, workspace_id: Uuid, user_id: Uuid) -> Result<()> {
let member = self.get_member(workspace_id, user_id).await?;
PermissionChecker::check(member.role, Permission::WorkspaceDelete)?;
let now = Utc::now();
sqlx::query!(
r#"
UPDATE workspaces
SET is_archived = TRUE, updated_at = ?
WHERE id = ?
"#,
now,
workspace_id
)
.execute(&self.db)
.await?;
self.cache.write().remove(&workspace_id);
Ok(())
}
pub async fn add_member(
&self,
workspace_id: Uuid,
user_id: Uuid,
new_member_id: Uuid,
role: UserRole,
) -> Result<WorkspaceMember> {
let member = self.get_member(workspace_id, user_id).await?;
PermissionChecker::check(member.role, Permission::InviteMembers)?;
let new_member = WorkspaceMember::new(workspace_id, new_member_id, role);
sqlx::query!(
r#"
INSERT INTO workspace_members (id, workspace_id, user_id, role, joined_at, last_activity)
VALUES (?, ?, ?, ?, ?, ?)
"#,
new_member.id,
new_member.workspace_id,
new_member.user_id,
new_member.role,
new_member.joined_at,
new_member.last_activity
)
.execute(&self.db)
.await?;
Ok(new_member)
}
pub async fn remove_member(
&self,
workspace_id: Uuid,
user_id: Uuid,
member_to_remove: Uuid,
) -> Result<()> {
let member = self.get_member(workspace_id, user_id).await?;
PermissionChecker::check(member.role, Permission::RemoveMembers)?;
let workspace = self.get_workspace(workspace_id).await?;
if member_to_remove == workspace.owner_id {
return Err(CollabError::InvalidInput("Cannot remove workspace owner".to_string()));
}
sqlx::query!(
r#"
DELETE FROM workspace_members
WHERE workspace_id = ? AND user_id = ?
"#,
workspace_id,
member_to_remove
)
.execute(&self.db)
.await?;
Ok(())
}
pub async fn change_role(
&self,
workspace_id: Uuid,
user_id: Uuid,
member_id: Uuid,
new_role: UserRole,
) -> Result<WorkspaceMember> {
let member = self.get_member(workspace_id, user_id).await?;
PermissionChecker::check(member.role, Permission::ChangeRoles)?;
let workspace = self.get_workspace(workspace_id).await?;
if member_id == workspace.owner_id {
return Err(CollabError::InvalidInput(
"Cannot change workspace owner's role".to_string(),
));
}
sqlx::query!(
r#"
UPDATE workspace_members
SET role = ?
WHERE workspace_id = ? AND user_id = ?
"#,
new_role,
workspace_id,
member_id
)
.execute(&self.db)
.await?;
self.get_member(workspace_id, member_id).await
}
pub async fn get_member(&self, workspace_id: Uuid, user_id: Uuid) -> Result<WorkspaceMember> {
sqlx::query_as!(
WorkspaceMember,
r#"
SELECT
id as "id: Uuid",
workspace_id as "workspace_id: Uuid",
user_id as "user_id: Uuid",
role as "role: UserRole",
joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
FROM workspace_members
WHERE workspace_id = ? AND user_id = ?
"#,
workspace_id,
user_id
)
.fetch_optional(&self.db)
.await?
.ok_or_else(|| CollabError::AuthorizationFailed("User is not a member".to_string()))
}
pub async fn list_members(&self, workspace_id: Uuid) -> Result<Vec<WorkspaceMember>> {
let members = sqlx::query_as!(
WorkspaceMember,
r#"
SELECT
id as "id: Uuid",
workspace_id as "workspace_id: Uuid",
user_id as "user_id: Uuid",
role as "role: UserRole",
joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
FROM workspace_members
WHERE workspace_id = ?
ORDER BY joined_at
"#,
workspace_id
)
.fetch_all(&self.db)
.await?;
Ok(members)
}
pub async fn list_user_workspaces(&self, user_id: Uuid) -> Result<Vec<TeamWorkspace>> {
let workspaces = sqlx::query_as!(
TeamWorkspace,
r#"
SELECT
w.id as "id: Uuid",
w.name,
w.description,
w.owner_id as "owner_id: Uuid",
w.config,
w.version,
w.created_at as "created_at: chrono::DateTime<chrono::Utc>",
w.updated_at as "updated_at: chrono::DateTime<chrono::Utc>",
w.is_archived as "is_archived: bool"
FROM workspaces w
INNER JOIN workspace_members m ON w.id = m.workspace_id
WHERE m.user_id = ? AND w.is_archived = FALSE
ORDER BY w.updated_at DESC
"#,
user_id
)
.fetch_all(&self.db)
.await?;
Ok(workspaces)
}
pub async fn fork_workspace(
&self,
source_workspace_id: Uuid,
new_name: Option<String>,
new_owner_id: Uuid,
fork_point_commit_id: Option<Uuid>,
) -> Result<TeamWorkspace> {
self.get_member(source_workspace_id, new_owner_id).await?;
let source_workspace = self.get_workspace(source_workspace_id).await?;
let mut forked_workspace = TeamWorkspace::new(
new_name.unwrap_or_else(|| format!("{} (Fork)", source_workspace.name)),
new_owner_id,
);
forked_workspace.description.clone_from(&source_workspace.description);
if let Some(core_bridge) = &self.core_bridge {
if let Ok(mut core_workspace) = core_bridge.team_to_core(&source_workspace) {
core_workspace.id = forked_workspace.id.to_string();
core_workspace.name.clone_from(&forked_workspace.name);
core_workspace.description.clone_from(&forked_workspace.description);
core_workspace.created_at = forked_workspace.created_at;
core_workspace.updated_at = forked_workspace.updated_at;
Self::regenerate_entity_ids(&mut core_workspace);
if let Ok(team_ws) = core_bridge.core_to_team(&core_workspace, new_owner_id) {
forked_workspace.config = team_ws.config;
} else {
forked_workspace.config.clone_from(&source_workspace.config);
}
} else {
forked_workspace.config.clone_from(&source_workspace.config);
}
} else {
forked_workspace.config = source_workspace.config.clone();
}
sqlx::query!(
r#"
INSERT INTO workspaces (id, name, description, owner_id, config, version, created_at, updated_at, is_archived)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
"#,
forked_workspace.id,
forked_workspace.name,
forked_workspace.description,
forked_workspace.owner_id,
forked_workspace.config,
forked_workspace.version,
forked_workspace.created_at,
forked_workspace.updated_at,
forked_workspace.is_archived
)
.execute(&self.db)
.await?;
let member = WorkspaceMember::new(forked_workspace.id, new_owner_id, UserRole::Admin);
sqlx::query!(
r#"
INSERT INTO workspace_members (id, workspace_id, user_id, role, joined_at, last_activity)
VALUES (?, ?, ?, ?, ?, ?)
"#,
member.id,
member.workspace_id,
member.user_id,
member.role,
member.joined_at,
member.last_activity
)
.execute(&self.db)
.await?;
let fork = WorkspaceFork::new(
source_workspace_id,
forked_workspace.id,
new_owner_id,
fork_point_commit_id,
);
sqlx::query!(
r#"
INSERT INTO workspace_forks (id, source_workspace_id, forked_workspace_id, forked_at, forked_by, fork_point_commit_id)
VALUES (?, ?, ?, ?, ?, ?)
"#,
fork.id,
fork.source_workspace_id,
fork.forked_workspace_id,
fork.forked_at,
fork.forked_by,
fork.fork_point_commit_id
)
.execute(&self.db)
.await?;
self.cache.write().insert(forked_workspace.id, forked_workspace.clone());
Ok(forked_workspace)
}
pub async fn list_forks(&self, workspace_id: Uuid) -> Result<Vec<WorkspaceFork>> {
let forks = sqlx::query_as!(
WorkspaceFork,
r#"
SELECT
id as "id: Uuid",
source_workspace_id as "source_workspace_id: Uuid",
forked_workspace_id as "forked_workspace_id: Uuid",
forked_at as "forked_at: chrono::DateTime<chrono::Utc>",
forked_by as "forked_by: Uuid",
fork_point_commit_id as "fork_point_commit_id: Uuid"
FROM workspace_forks
WHERE source_workspace_id = ?
ORDER BY forked_at DESC
"#,
workspace_id
)
.fetch_all(&self.db)
.await?;
Ok(forks)
}
pub async fn get_fork_source(
&self,
forked_workspace_id: Uuid,
) -> Result<Option<WorkspaceFork>> {
let fork = sqlx::query_as!(
WorkspaceFork,
r#"
SELECT
id as "id: Uuid",
source_workspace_id as "source_workspace_id: Uuid",
forked_workspace_id as "forked_workspace_id: Uuid",
forked_at as "forked_at: chrono::DateTime<chrono::Utc>",
forked_by as "forked_by: Uuid",
fork_point_commit_id as "fork_point_commit_id: Uuid"
FROM workspace_forks
WHERE forked_workspace_id = ?
"#,
forked_workspace_id
)
.fetch_optional(&self.db)
.await?;
Ok(fork)
}
#[allow(clippy::items_after_statements)]
fn regenerate_entity_ids(core_workspace: &mut mockforge_core::workspace::Workspace) {
use mockforge_core::workspace::Folder;
use uuid::Uuid;
core_workspace.id = Uuid::new_v4().to_string();
fn regenerate_folder_ids(folder: &mut Folder) {
folder.id = Uuid::new_v4().to_string();
for subfolder in &mut folder.folders {
regenerate_folder_ids(subfolder);
}
for request in &mut folder.requests {
request.id = Uuid::new_v4().to_string();
}
}
for folder in &mut core_workspace.folders {
regenerate_folder_ids(folder);
}
for request in &mut core_workspace.requests {
request.id = Uuid::new_v4().to_string();
}
}
}
pub struct WorkspaceManager {
service: Arc<WorkspaceService>,
}
impl WorkspaceManager {
#[must_use]
pub const fn new(service: Arc<WorkspaceService>) -> Self {
Self { service }
}
pub async fn create_workspace(
&self,
name: String,
description: Option<String>,
owner_id: Uuid,
) -> Result<TeamWorkspace> {
self.service.create_workspace(name, description, owner_id).await
}
pub async fn get_workspace(&self, workspace_id: Uuid, user_id: Uuid) -> Result<TeamWorkspace> {
self.service.get_member(workspace_id, user_id).await?;
self.service.get_workspace(workspace_id).await
}
}
#[cfg(test)]
mod tests {
}