diff --git a/migrations/versions/8e4b4b8d1a88_add_soft_delete.py b/migrations/versions/8e4b4b8d1a88_add_soft_delete.py new file mode 100644 index 00000000..ad8138a3 --- /dev/null +++ b/migrations/versions/8e4b4b8d1a88_add_soft_delete.py @@ -0,0 +1,30 @@ +"""add soft delete + +Revision ID: 8e4b4b8d1a88 +Revises: 5c2f3eee5f90 +Create Date: 2025-01-20 14:08:40.851647 + +""" + +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "8e4b4b8d1a88" +down_revision: Union[str, None] = "5c2f3eee5f90" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.execute( + """ + ALTER TABLE workspaces + ADD COLUMN deleted_at DATETIME DEFAULT NULL; + """ + ) + + +def downgrade() -> None: + op.execute("ALTER TABLE workspaces DROP COLUMN deleted_at;") diff --git a/migrations/versions/e6227073183d_merging_system_prompt_and_soft_deletes.py b/migrations/versions/e6227073183d_merging_system_prompt_and_soft_deletes.py new file mode 100644 index 00000000..64dd0717 --- /dev/null +++ b/migrations/versions/e6227073183d_merging_system_prompt_and_soft_deletes.py @@ -0,0 +1,23 @@ +"""merging system prompt and soft-deletes + +Revision ID: e6227073183d +Revises: 8e4b4b8d1a88, a692c8b52308 +Create Date: 2025-01-20 16:08:40.645298 + +""" + +from typing import Sequence, Union + +# revision identifiers, used by Alembic. +revision: str = "e6227073183d" +down_revision: Union[str, None] = ("8e4b4b8d1a88", "a692c8b52308") +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass diff --git a/src/codegate/api/v1.py b/src/codegate/api/v1.py index 73a8d195..363e2f47 100644 --- a/src/codegate/api/v1.py +++ b/src/codegate/api/v1.py @@ -79,8 +79,14 @@ async def create_workspace(request: v1_models.CreateWorkspaceRequest) -> v1_mode "/workspaces/{workspace_name}", tags=["Workspaces"], generate_unique_id_function=uniq_name, - status_code=204, ) async def delete_workspace(workspace_name: str): """Delete a workspace by name.""" - raise NotImplementedError + try: + _ = await wscrud.soft_delete_workspace(workspace_name) + except crud.WorkspaceDoesNotExistError: + raise HTTPException(status_code=404, detail="Workspace does not exist") + except Exception: + raise HTTPException(status_code=500, detail="Internal server error") + + return Response(status_code=204) diff --git a/src/codegate/db/connection.py b/src/codegate/db/connection.py index cca4e691..6e014916 100644 --- a/src/codegate/db/connection.py +++ b/src/codegate/db/connection.py @@ -304,6 +304,20 @@ async def update_session(self, session: Session) -> Optional[Session]: active_session = await self._execute_update_pydantic_model(session, sql, should_raise=True) return active_session + async def soft_delete_workspace(self, workspace: Workspace) -> Optional[Workspace]: + sql = text( + """ + UPDATE workspaces + SET deleted_at = CURRENT_TIMESTAMP + WHERE id = :id + RETURNING * + """ + ) + deleted_workspace = await self._execute_update_pydantic_model( + workspace, sql, should_raise=True + ) + return deleted_workspace + class DbReader(DbCodeGate): @@ -401,6 +415,7 @@ async def get_workspaces(self) -> List[WorkspaceActive]: w.id, w.name, s.active_workspace_id FROM workspaces w LEFT JOIN sessions s ON w.id = s.active_workspace_id + WHERE w.deleted_at IS NULL """ ) workspaces = await self._execute_select_pydantic_model(WorkspaceActive, sql) @@ -412,7 +427,7 @@ async def get_workspace_by_name(self, name: str) -> Optional[Workspace]: SELECT id, name, system_prompt FROM workspaces - WHERE name = :name + WHERE name = :name AND deleted_at IS NULL """ ) conditions = {"name": name} diff --git a/src/codegate/pipeline/cli/commands.py b/src/codegate/pipeline/cli/commands.py index 8902f409..233188ba 100644 --- a/src/codegate/pipeline/cli/commands.py +++ b/src/codegate/pipeline/cli/commands.py @@ -153,6 +153,7 @@ def subcommands(self) -> Dict[str, Callable[[List[str]], Awaitable[str]]]: "list": self._list_workspaces, "add": self._add_workspace, "activate": self._activate_workspace, + "remove": self._remove_workspace, } async def _list_workspaces(self, flags: Dict[str, str], args: List[str]) -> str: @@ -211,6 +212,25 @@ async def _activate_workspace(self, flags: Dict[str, str], args: List[str]) -> s return "An error occurred while activating the workspace" return f"Workspace **{workspace_name}** has been activated" + async def _remove_workspace(self, flags: Dict[str, str], args: List[str]) -> str: + """ + Remove a workspace + """ + if args is None or len(args) == 0: + return "Please provide a name. Use `codegate workspace remove workspace_name`" + + workspace_name = args[0] + if not workspace_name: + return "Please provide a name. Use `codegate workspace remove workspace_name`" + + try: + await self.workspace_crud.soft_delete_workspace(workspace_name) + except crud.WorkspaceDoesNotExistError: + return f"Workspace **{workspace_name}** does not exist" + except Exception: + return "An error occurred while removing the workspace" + return f"Workspace **{workspace_name}** has been removed" + @property def help(self) -> str: return ( diff --git a/src/codegate/workspaces/crud.py b/src/codegate/workspaces/crud.py index 2b44466d..9fcc63de 100644 --- a/src/codegate/workspaces/crud.py +++ b/src/codegate/workspaces/crud.py @@ -68,10 +68,6 @@ async def _is_workspace_active( async def activate_workspace(self, workspace_name: str): """ Activate a workspace - - Will return: - - True if the workspace was activated - - False if the workspace is already active or does not exist """ is_active, session, workspace = await self._is_workspace_active(workspace_name) if is_active: @@ -100,6 +96,31 @@ async def update_workspace_system_prompt( updated_workspace = await db_recorder.update_workspace(workspace_update) return updated_workspace + async def soft_delete_workspace(self, workspace_name: str): + """ + Soft delete a workspace + """ + if workspace_name == "": + raise WorkspaceCrudError("Workspace name cannot be empty.") + if workspace_name == "default": + raise WorkspaceCrudError("Cannot delete default workspace.") + + selected_workspace = await self._db_reader.get_workspace_by_name(workspace_name) + if not selected_workspace: + raise WorkspaceDoesNotExistError(f"Workspace {workspace_name} does not exist.") + + # Check if workspace is active, if it is, make the default workspace active + active_workspace = await self._db_reader.get_active_workspace() + if active_workspace and active_workspace.id == selected_workspace.id: + raise WorkspaceCrudError("Cannot delete active workspace.") + + db_recorder = DbRecorder() + try: + _ = await db_recorder.soft_delete_workspace(selected_workspace) + except Exception: + raise WorkspaceCrudError(f"Error deleting workspace {workspace_name}") + return + async def get_workspace_by_name(self, workspace_name: str) -> Workspace: workspace = await self._db_reader.get_workspace_by_name(workspace_name) if not workspace:
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: