added redb and moved workspaces to use db. using tags instead of environment for workspace
This commit is contained in:
572
src-tauri/src/workspaces/workspace.rs
Normal file
572
src-tauri/src/workspaces/workspace.rs
Normal file
@@ -0,0 +1,572 @@
|
||||
use chrono::Utc;
|
||||
use redb::ReadableTable;
|
||||
|
||||
use crate::db::{
|
||||
Database, DbError, DbResult, WORKSPACES, WORKSPACE_SYNC_GROUPS, WORKSPACES_BY_SYNC_GROUP,
|
||||
};
|
||||
|
||||
use super::types::{
|
||||
CreateSyncGroupInput, CreateWorkspaceInput, UpdateSyncGroupInput, UpdateWorkspaceInput,
|
||||
Workspace, WorkspaceSyncGroup,
|
||||
};
|
||||
|
||||
pub struct WorkspaceService {
|
||||
db: Database,
|
||||
}
|
||||
|
||||
impl WorkspaceService {
|
||||
pub fn new(db: Database) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub fn get_all(&self) -> DbResult<Vec<Workspace>> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
let table = read_txn.open_table(WORKSPACES)?;
|
||||
|
||||
let mut workspaces = Vec::new();
|
||||
for entry in table.iter()? {
|
||||
let (_, value) = entry?;
|
||||
let workspace: Workspace = serde_json::from_str(value.value())
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
workspaces.push(workspace);
|
||||
}
|
||||
|
||||
workspaces.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(workspaces)
|
||||
}
|
||||
|
||||
pub fn get(&self, id: &str) -> DbResult<Workspace> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
let table = read_txn.open_table(WORKSPACES)?;
|
||||
|
||||
let value = table
|
||||
.get(id)?
|
||||
.ok_or_else(|| DbError::NotFound(format!("Workspace not found: {}", id)))?;
|
||||
|
||||
let workspace: Workspace = serde_json::from_str(value.value())
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
Ok(workspace)
|
||||
}
|
||||
|
||||
pub fn create(&self, input: CreateWorkspaceInput) -> DbResult<Workspace> {
|
||||
let mut workspace = Workspace::new(input.name, input.description);
|
||||
workspace.tags = input.tags;
|
||||
|
||||
let json = serde_json::to_string(&workspace)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
let write_txn = self.db.begin_write()?;
|
||||
{
|
||||
let mut table = write_txn.open_table(WORKSPACES)?;
|
||||
table.insert(workspace.id.as_str(), json.as_str())?;
|
||||
}
|
||||
write_txn.commit()?;
|
||||
|
||||
Ok(workspace)
|
||||
}
|
||||
|
||||
pub fn update(&self, input: UpdateWorkspaceInput) -> DbResult<Workspace> {
|
||||
let mut workspace = self.get(&input.id)?;
|
||||
|
||||
if let Some(name) = input.name {
|
||||
workspace.name = name;
|
||||
}
|
||||
if let Some(description) = input.description {
|
||||
workspace.description = description;
|
||||
}
|
||||
if let Some(tags) = input.tags {
|
||||
workspace.tags = tags;
|
||||
}
|
||||
if let Some(sync_group_id) = input.sync_group_id {
|
||||
workspace.sync_group_id = Some(sync_group_id);
|
||||
}
|
||||
workspace.updated_at = Utc::now();
|
||||
|
||||
// Write back
|
||||
let json = serde_json::to_string(&workspace)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
let write_txn = self.db.begin_write()?;
|
||||
{
|
||||
let mut table = write_txn.open_table(WORKSPACES)?;
|
||||
table.insert(workspace.id.as_str(), json.as_str())?;
|
||||
}
|
||||
write_txn.commit()?;
|
||||
|
||||
Ok(workspace)
|
||||
}
|
||||
|
||||
/// Delete a workspace
|
||||
pub fn delete(&self, id: &str) -> DbResult<()> {
|
||||
// First get the workspace to check sync_group_id
|
||||
let workspace = self.get(id)?;
|
||||
let sync_group_id = workspace.sync_group_id.clone();
|
||||
|
||||
let write_txn = self.db.begin_write()?;
|
||||
|
||||
// Remove from workspaces table
|
||||
{
|
||||
let mut table = write_txn.open_table(WORKSPACES)?;
|
||||
table.remove(id)?;
|
||||
}
|
||||
|
||||
// Remove from sync group index if applicable
|
||||
if let Some(group_id) = sync_group_id {
|
||||
self.remove_from_sync_index(&write_txn, &group_id, id)?;
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper to remove a workspace ID from the sync group index
|
||||
fn remove_from_sync_index(
|
||||
&self,
|
||||
write_txn: &redb::WriteTransaction,
|
||||
group_id: &str,
|
||||
workspace_id: &str,
|
||||
) -> DbResult<()> {
|
||||
let mut idx_table = write_txn.open_table(WORKSPACES_BY_SYNC_GROUP)?;
|
||||
|
||||
// Read current IDs
|
||||
let ids_json = match idx_table.get(group_id)? {
|
||||
Some(value) => value.value().to_string(),
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let mut ids: Vec<String> = serde_json::from_str(&ids_json)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
ids.retain(|i| i != workspace_id);
|
||||
|
||||
let new_json = serde_json::to_string(&ids)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
idx_table.insert(group_id, new_json.as_str())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper to add a workspace ID to the sync group index
|
||||
fn add_to_sync_index(
|
||||
&self,
|
||||
write_txn: &redb::WriteTransaction,
|
||||
group_id: &str,
|
||||
workspace_id: &str,
|
||||
) -> DbResult<()> {
|
||||
let mut idx_table = write_txn.open_table(WORKSPACES_BY_SYNC_GROUP)?;
|
||||
|
||||
// Read current IDs or start with empty
|
||||
let ids_json = match idx_table.get(group_id)? {
|
||||
Some(value) => value.value().to_string(),
|
||||
None => "[]".to_string(),
|
||||
};
|
||||
|
||||
let mut ids: Vec<String> = serde_json::from_str(&ids_json)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
if !ids.contains(&workspace_id.to_string()) {
|
||||
ids.push(workspace_id.to_string());
|
||||
}
|
||||
|
||||
let new_json = serde_json::to_string(&ids)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
idx_table.insert(group_id, new_json.as_str())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ==================== Sync Group Operations ====================
|
||||
|
||||
/// Get all sync groups
|
||||
pub fn get_all_sync_groups(&self) -> DbResult<Vec<WorkspaceSyncGroup>> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
let table = read_txn.open_table(WORKSPACE_SYNC_GROUPS)?;
|
||||
|
||||
let mut groups = Vec::new();
|
||||
for entry in table.iter()? {
|
||||
let (_, value) = entry?;
|
||||
let group: WorkspaceSyncGroup = serde_json::from_str(value.value())
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
groups.push(group);
|
||||
}
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
/// Get a sync group by ID
|
||||
pub fn get_sync_group(&self, id: &str) -> DbResult<WorkspaceSyncGroup> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
let table = read_txn.open_table(WORKSPACE_SYNC_GROUPS)?;
|
||||
|
||||
let value = table
|
||||
.get(id)?
|
||||
.ok_or_else(|| DbError::NotFound(format!("Sync group not found: {}", id)))?;
|
||||
|
||||
let group: WorkspaceSyncGroup = serde_json::from_str(value.value())
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
/// Get sync group for a workspace
|
||||
pub fn get_sync_group_for_workspace(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
) -> DbResult<Option<WorkspaceSyncGroup>> {
|
||||
let workspace = self.get(workspace_id)?;
|
||||
|
||||
match workspace.sync_group_id {
|
||||
Some(group_id) => Ok(Some(self.get_sync_group(&group_id)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new sync group
|
||||
pub fn create_sync_group(&self, input: CreateSyncGroupInput) -> DbResult<WorkspaceSyncGroup> {
|
||||
let mut group = WorkspaceSyncGroup::new(input.name, input.workspace_ids.clone());
|
||||
group.synced_variable_names = input.synced_variable_names;
|
||||
group.sync_secrets = input.sync_secrets;
|
||||
|
||||
let json = serde_json::to_string(&group)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
let write_txn = self.db.begin_write()?;
|
||||
|
||||
// Insert sync group
|
||||
{
|
||||
let mut table = write_txn.open_table(WORKSPACE_SYNC_GROUPS)?;
|
||||
table.insert(group.id.as_str(), json.as_str())?;
|
||||
}
|
||||
|
||||
// Update index
|
||||
{
|
||||
let mut idx_table = write_txn.open_table(WORKSPACES_BY_SYNC_GROUP)?;
|
||||
let ids_json = serde_json::to_string(&input.workspace_ids)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
idx_table.insert(group.id.as_str(), ids_json.as_str())?;
|
||||
}
|
||||
|
||||
// Update each workspace's sync_group_id
|
||||
{
|
||||
let mut ws_table = write_txn.open_table(WORKSPACES)?;
|
||||
for ws_id in &input.workspace_ids {
|
||||
// Read workspace
|
||||
let ws_json = match ws_table.get(ws_id.as_str())? {
|
||||
Some(value) => value.value().to_string(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let mut workspace: Workspace = serde_json::from_str(&ws_json)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
workspace.sync_group_id = Some(group.id.clone());
|
||||
workspace.updated_at = Utc::now();
|
||||
|
||||
let new_ws_json = serde_json::to_string(&workspace)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
ws_table.insert(ws_id.as_str(), new_ws_json.as_str())?;
|
||||
}
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
/// Update a sync group
|
||||
pub fn update_sync_group(&self, input: UpdateSyncGroupInput) -> DbResult<WorkspaceSyncGroup> {
|
||||
// Read existing
|
||||
let mut group = self.get_sync_group(&input.id)?;
|
||||
|
||||
// Apply updates
|
||||
if let Some(name) = input.name {
|
||||
group.name = name;
|
||||
}
|
||||
if let Some(synced_variable_names) = input.synced_variable_names {
|
||||
group.synced_variable_names = synced_variable_names;
|
||||
}
|
||||
if let Some(sync_secrets) = input.sync_secrets {
|
||||
group.sync_secrets = sync_secrets;
|
||||
}
|
||||
group.updated_at = Utc::now();
|
||||
|
||||
// Write back
|
||||
let json = serde_json::to_string(&group)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
let write_txn = self.db.begin_write()?;
|
||||
{
|
||||
let mut table = write_txn.open_table(WORKSPACE_SYNC_GROUPS)?;
|
||||
table.insert(group.id.as_str(), json.as_str())?;
|
||||
}
|
||||
write_txn.commit()?;
|
||||
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
/// Delete a sync group
|
||||
pub fn delete_sync_group(&self, id: &str) -> DbResult<()> {
|
||||
// Get the sync group to find associated workspaces
|
||||
let group = self.get_sync_group(id)?;
|
||||
let workspace_ids = group.workspace_ids.clone();
|
||||
|
||||
let write_txn = self.db.begin_write()?;
|
||||
|
||||
// Remove sync_group_id from all associated workspaces
|
||||
{
|
||||
let mut ws_table = write_txn.open_table(WORKSPACES)?;
|
||||
for ws_id in &workspace_ids {
|
||||
let ws_json = match ws_table.get(ws_id.as_str())? {
|
||||
Some(value) => value.value().to_string(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let mut workspace: Workspace = serde_json::from_str(&ws_json)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
workspace.sync_group_id = None;
|
||||
workspace.updated_at = Utc::now();
|
||||
|
||||
let new_ws_json = serde_json::to_string(&workspace)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
ws_table.insert(ws_id.as_str(), new_ws_json.as_str())?;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from sync groups table
|
||||
{
|
||||
let mut table = write_txn.open_table(WORKSPACE_SYNC_GROUPS)?;
|
||||
table.remove(id)?;
|
||||
}
|
||||
|
||||
// Remove from index
|
||||
{
|
||||
let mut idx_table = write_txn.open_table(WORKSPACES_BY_SYNC_GROUP)?;
|
||||
idx_table.remove(id)?;
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get workspaces by sync group
|
||||
pub fn get_workspaces_by_sync_group(&self, sync_group_id: &str) -> DbResult<Vec<Workspace>> {
|
||||
let read_txn = self.db.begin_read()?;
|
||||
let idx_table = read_txn.open_table(WORKSPACES_BY_SYNC_GROUP)?;
|
||||
|
||||
let workspace_ids: Vec<String> = match idx_table.get(sync_group_id)? {
|
||||
Some(value) => serde_json::from_str(value.value())
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
|
||||
drop(idx_table);
|
||||
drop(read_txn);
|
||||
|
||||
let mut workspaces = Vec::new();
|
||||
for ws_id in workspace_ids {
|
||||
if let Ok(workspace) = self.get(&ws_id) {
|
||||
workspaces.push(workspace);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(workspaces)
|
||||
}
|
||||
|
||||
/// Add a workspace to a sync group
|
||||
pub fn add_workspace_to_sync_group(
|
||||
&self,
|
||||
sync_group_id: &str,
|
||||
workspace_id: &str,
|
||||
) -> DbResult<()> {
|
||||
// Read existing data
|
||||
let mut group = self.get_sync_group(sync_group_id)?;
|
||||
let mut workspace = self.get(workspace_id)?;
|
||||
|
||||
// Update in memory
|
||||
if !group.workspace_ids.contains(&workspace_id.to_string()) {
|
||||
group.workspace_ids.push(workspace_id.to_string());
|
||||
group.updated_at = Utc::now();
|
||||
}
|
||||
workspace.sync_group_id = Some(sync_group_id.to_string());
|
||||
workspace.updated_at = Utc::now();
|
||||
|
||||
// Serialize
|
||||
let group_json = serde_json::to_string(&group)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
let ws_json = serde_json::to_string(&workspace)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
// Write all changes
|
||||
let write_txn = self.db.begin_write()?;
|
||||
|
||||
{
|
||||
let mut table = write_txn.open_table(WORKSPACE_SYNC_GROUPS)?;
|
||||
table.insert(sync_group_id, group_json.as_str())?;
|
||||
}
|
||||
|
||||
self.add_to_sync_index(&write_txn, sync_group_id, workspace_id)?;
|
||||
|
||||
{
|
||||
let mut ws_table = write_txn.open_table(WORKSPACES)?;
|
||||
ws_table.insert(workspace_id, ws_json.as_str())?;
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a workspace from a sync group
|
||||
pub fn remove_workspace_from_sync_group(
|
||||
&self,
|
||||
sync_group_id: &str,
|
||||
workspace_id: &str,
|
||||
) -> DbResult<()> {
|
||||
// Read existing data
|
||||
let mut group = self.get_sync_group(sync_group_id)?;
|
||||
let mut workspace = self.get(workspace_id)?;
|
||||
|
||||
// Update in memory
|
||||
group.workspace_ids.retain(|id| id != workspace_id);
|
||||
group.updated_at = Utc::now();
|
||||
workspace.sync_group_id = None;
|
||||
workspace.updated_at = Utc::now();
|
||||
|
||||
// Serialize
|
||||
let group_json = serde_json::to_string(&group)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
let ws_json = serde_json::to_string(&workspace)
|
||||
.map_err(|e| DbError::Serialization(e.to_string()))?;
|
||||
|
||||
// Write all changes
|
||||
let write_txn = self.db.begin_write()?;
|
||||
|
||||
{
|
||||
let mut table = write_txn.open_table(WORKSPACE_SYNC_GROUPS)?;
|
||||
table.insert(sync_group_id, group_json.as_str())?;
|
||||
}
|
||||
|
||||
self.remove_from_sync_index(&write_txn, sync_group_id, workspace_id)?;
|
||||
|
||||
{
|
||||
let mut ws_table = write_txn.open_table(WORKSPACES)?;
|
||||
ws_table.insert(workspace_id, ws_json.as_str())?;
|
||||
}
|
||||
|
||||
write_txn.commit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::env::temp_dir;
|
||||
|
||||
fn create_test_db() -> Database {
|
||||
let path = temp_dir().join(format!("resona_test_{}.redb", uuid::Uuid::new_v4()));
|
||||
Database::open_at(path).expect("Failed to create test database")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_workspace_crud() {
|
||||
let db = create_test_db();
|
||||
let service = WorkspaceService::new(db);
|
||||
|
||||
let workspace = service
|
||||
.create(CreateWorkspaceInput {
|
||||
name: "Test Workspace".to_string(),
|
||||
description: "A test workspace".to_string(),
|
||||
tags: vec!["Development".to_string()],
|
||||
})
|
||||
.expect("Failed to create workspace");
|
||||
|
||||
assert_eq!(workspace.name, "Test Workspace");
|
||||
assert_eq!(workspace.tags, vec!["Development".to_string()]);
|
||||
|
||||
let fetched = service.get(&workspace.id).expect("Failed to get workspace");
|
||||
assert_eq!(fetched.id, workspace.id);
|
||||
|
||||
let updated = service
|
||||
.update(UpdateWorkspaceInput {
|
||||
id: workspace.id.clone(),
|
||||
name: Some("Updated Workspace".to_string()),
|
||||
description: None,
|
||||
tags: None,
|
||||
sync_group_id: None,
|
||||
})
|
||||
.expect("Failed to update workspace");
|
||||
|
||||
assert_eq!(updated.name, "Updated Workspace");
|
||||
assert_eq!(updated.description, "A test workspace");
|
||||
|
||||
let all = service.get_all().expect("Failed to get all workspaces");
|
||||
assert_eq!(all.len(), 1);
|
||||
|
||||
service
|
||||
.delete(&workspace.id)
|
||||
.expect("Failed to delete workspace");
|
||||
let all = service.get_all().expect("Failed to get all workspaces");
|
||||
assert_eq!(all.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_groups() {
|
||||
let db = create_test_db();
|
||||
let service = WorkspaceService::new(db);
|
||||
|
||||
let ws1 = service
|
||||
.create(CreateWorkspaceInput {
|
||||
name: "Workspace 1".to_string(),
|
||||
description: "First workspace".to_string(),
|
||||
tags: vec!["Development".to_string()],
|
||||
})
|
||||
.expect("Failed to create workspace 1");
|
||||
|
||||
let ws2 = service
|
||||
.create(CreateWorkspaceInput {
|
||||
name: "Workspace 2".to_string(),
|
||||
description: "Second workspace".to_string(),
|
||||
tags: vec!["Production".to_string()],
|
||||
})
|
||||
.expect("Failed to create workspace 2");
|
||||
|
||||
// Create sync group
|
||||
let group = service
|
||||
.create_sync_group(CreateSyncGroupInput {
|
||||
name: "Test Sync Group".to_string(),
|
||||
workspace_ids: vec![ws1.id.clone(), ws2.id.clone()],
|
||||
synced_variable_names: vec!["API_KEY".to_string()],
|
||||
sync_secrets: false,
|
||||
})
|
||||
.expect("Failed to create sync group");
|
||||
|
||||
// Verify workspaces are linked
|
||||
let ws1_updated = service.get(&ws1.id).expect("Failed to get workspace 1");
|
||||
assert_eq!(ws1_updated.sync_group_id, Some(group.id.clone()));
|
||||
|
||||
// Get workspaces by sync group
|
||||
let grouped = service
|
||||
.get_workspaces_by_sync_group(&group.id)
|
||||
.expect("Failed to get workspaces by sync group");
|
||||
assert_eq!(grouped.len(), 2);
|
||||
|
||||
// Delete sync group
|
||||
service
|
||||
.delete_sync_group(&group.id)
|
||||
.expect("Failed to delete sync group");
|
||||
|
||||
// Verify workspaces are unlinked
|
||||
let ws1_final = service.get(&ws1.id).expect("Failed to get workspace 1");
|
||||
assert_eq!(ws1_final.sync_group_id, None);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user