Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions agent-tasks/project-deletion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Project Deletion: Backend-Only Plan

## Scope
- Add backend support to soft-delete a project via a new API endpoint.
- UI work is out of scope (lives in a separate repo).

## Current State
- Backend exposes GET, PUT for projects; no DELETE yet (`backend/bootstrap/main.go`).
- Model `Project` uses `gorm.Model` (has `DeletedAt`) so soft-delete is supported by default.

## API
- Method: DELETE
- Path: `/api/projects/:project_id/`
- Headers: same as existing projects endpoints (Authorization, DIGGER_ORG_ID, DIGGER_USER_ID, DIGGER_ORG_SOURCE).
- Responses: 204 on success; 404 not found; 403 forbidden; 500 on error.

## Backend Steps
1) Route: In `backend/bootstrap/main.go`, register `projectsApiGroup.DELETE("/:project_id/", controllers.DeleteProjectApi)` inside the `if enableApi` block.
2) Controller: In `backend/controllers/projects.go`, add `DeleteProjectApi`:
- Resolve org from headers (`ORGANISATION_ID_KEY`, `ORGANISATION_SOURCE_KEY`).
- Load org by `external_id` + `external_source`.
- Load project by `projects.organisation_id = org.ID AND projects.id = :project_id`.
- Soft delete via `models.DB.GormDB.Delete(&project)` and return `204`.
3) Migrations: none (soft-delete already present and indexed).

## Out of Scope
- Any UI wiring or server functions in the UI repo.

## Security
- Endpoint is protected by existing API middleware (`InternalApiAuth`, `HeadersApiAuth`).

## Acceptance Criteria
- DELETE `/api/projects/:project_id/` returns 204 on success.
- After deletion, `GET /api/projects/` no longer lists the project (GORM soft-delete filtering applies).
- `GET /api/projects/:project_id/` returns 404 for the deleted project.

## Test Plan
- API: DELETE with valid headers → 204; repeat → 404; missing/invalid headers → 403.
- API: Verify project disappears from list endpoint; details endpoint returns 404 post-delete.

## Estimate
- Backend: 1–2 hours.

## Pointers
- Routes: `backend/bootstrap/main.go`
- Controller: `backend/controllers/projects.go`
- Model: `backend/models/orgs.go`
1 change: 1 addition & 0 deletions backend/bootstrap/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ func Bootstrap(templates embed.FS, diggerController controllers.DiggerController
projectsApiGroup.GET("/", controllers.ListProjectsApi)
projectsApiGroup.GET("/:project_id/", controllers.ProjectsDetailsApi)
projectsApiGroup.PUT("/:project_id/", controllers.UpdateProjectApi)
projectsApiGroup.DELETE("/:project_id/", controllers.DeleteProjectApi)

githubApiGroup := apiGroup.Group("/github")
githubApiGroup.POST("/link", controllers.LinkGithubInstallationToOrgApi)
Expand Down
42 changes: 42 additions & 0 deletions backend/controllers/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,48 @@ func UpdateProjectApi(c *gin.Context) {
c.JSON(http.StatusOK, project)
}

func DeleteProjectApi(c *gin.Context) {
// assume all exists as validated in middleware
organisationId := c.GetString(middleware.ORGANISATION_ID_KEY)
organisationSource := c.GetString(middleware.ORGANISATION_SOURCE_KEY)
projectId := c.Param("project_id")

var org models.Organisation
err := models.DB.GormDB.Where("external_id = ? AND external_source = ?", organisationId, organisationSource).First(&org).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
slog.Info("Organisation not found", "organisationId", organisationId, "source", organisationSource)
c.String(http.StatusNotFound, "Could not find organisation: "+organisationId)
} else {
slog.Error("Error fetching organisation", "organisationId", organisationId, "source", organisationSource, "error", err)
c.String(http.StatusInternalServerError, "Error fetching organisation")
}
return
}

var project models.Project
err = models.DB.GormDB.Where("projects.organisation_id = ? AND projects.id = ?", org.ID, projectId).First(&project).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
slog.Info("Project not found", "organisationId", organisationId, "orgId", org.ID)
c.String(http.StatusNotFound, "Could not find project")
} else {
slog.Error("Error fetching project", "organisationId", organisationId, "orgId", org.ID, "error", err)
c.String(http.StatusInternalServerError, "Unknown error occurred while fetching database")
}
return
}

err = models.DB.GormDB.Delete(&project).Error
if err != nil {
slog.Error("Error deleting project", "organisationId", organisationId, "orgId", org.ID, "projectId", projectId, "error", err)
c.String(http.StatusInternalServerError, "Unknown error occurred while deleting project")
return
}

c.Status(http.StatusNoContent)
}

func FindProjectsForRepo(c *gin.Context) {
repo := c.Param("repo")
orgId, exists := c.Get(middleware.ORGANISATION_ID_KEY)
Expand Down
Loading