Files
kandev/apps/backend/internal/azuredevops/controller.go
T
2026-07-21 08:40:26 +01:00

331 lines
9.5 KiB
Go

package azuredevops
import (
"context"
"errors"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/kandev/kandev/internal/common/logger"
)
const notConfiguredCode = "azure_devops_not_configured"
type Controller struct {
service *Service
log *logger.Logger
}
func RegisterRoutes(router *gin.Engine, service *Service, log *logger.Logger) {
controller := &Controller{service: service, log: log}
api := router.Group("/api/v1/azure-devops")
api.GET("/config", controller.getConfig)
api.POST("/config", controller.setConfig)
api.DELETE("/config", controller.deleteConfig)
api.POST("/config/test", controller.testConfig)
api.POST("/config/copy", controller.copyConfig)
api.GET("/views", controller.getSavedViews)
api.PUT("/views", controller.setSavedViews)
api.GET("/projects", controller.listProjects)
api.GET("/repositories", controller.listRepositories)
api.GET("/branches", controller.listBranches)
api.POST("/work-items/search", controller.searchWorkItems)
api.GET("/work-items/:id", controller.getWorkItem)
api.GET("/pull-requests", controller.listPullRequests)
api.GET("/pull-requests/:projectId/:repositoryId/:pullRequestId", controller.getPullRequest)
api.GET("/pull-requests/:projectId/:repositoryId/:pullRequestId/feedback", controller.getPullRequestFeedback)
api.GET("/workspaces/:workspaceId/task-prs", controller.listWorkspaceTaskPRs)
api.POST("/tasks/:taskId/pull-requests", controller.associateTaskPR)
api.POST("/tasks/:taskId/pull-requests/sync", controller.syncTaskPR)
}
func (c *Controller) getSavedViews(ctx *gin.Context) {
views, err := c.service.GetSavedViewsForWorkspace(ctx.Request.Context(), workspaceID(ctx))
if err != nil {
c.writeError(ctx, err)
return
}
ctx.JSON(http.StatusOK, gin.H{"views": views})
}
func (c *Controller) setSavedViews(ctx *gin.Context) {
var request struct {
Views []SavedView `json:"views"`
}
if err := ctx.ShouldBindJSON(&request); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
return
}
views, err := c.service.SetSavedViewsForWorkspace(
ctx.Request.Context(), workspaceID(ctx), request.Views,
)
if err != nil {
c.writeError(ctx, err)
return
}
ctx.JSON(http.StatusOK, gin.H{"views": views})
}
func (c *Controller) getConfig(ctx *gin.Context) {
cfg, err := c.service.GetConfigForWorkspace(ctx.Request.Context(), workspaceID(ctx))
if err != nil {
c.writeError(ctx, err)
return
}
if cfg == nil {
ctx.Status(http.StatusNoContent)
return
}
ctx.JSON(http.StatusOK, cfg)
}
func (c *Controller) setConfig(ctx *gin.Context) {
var request SetConfigRequest
if err := ctx.ShouldBindJSON(&request); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
return
}
cfg, err := c.service.SetConfigForWorkspace(ctx.Request.Context(), workspaceID(ctx), &request)
if err != nil {
c.writeError(ctx, err)
return
}
ctx.JSON(http.StatusOK, cfg)
}
func (c *Controller) deleteConfig(ctx *gin.Context) {
if err := c.service.DeleteConfigForWorkspace(ctx.Request.Context(), workspaceID(ctx)); err != nil {
c.writeError(ctx, err)
return
}
ctx.JSON(http.StatusOK, gin.H{"deleted": true})
}
func (c *Controller) testConfig(ctx *gin.Context) {
var request SetConfigRequest
if ctx.Request.ContentLength != 0 {
if err := ctx.ShouldBindJSON(&request); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
return
}
}
result, err := c.service.TestConnectionForWorkspace(ctx.Request.Context(), workspaceID(ctx), &request)
if err != nil {
c.writeError(ctx, err)
return
}
ctx.JSON(http.StatusOK, result)
}
func (c *Controller) copyConfig(ctx *gin.Context) {
var request struct {
TargetWorkspaceID string `json:"targetWorkspaceId"`
}
if err := ctx.ShouldBindJSON(&request); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
return
}
cfg, err := c.service.CopyConfigToWorkspace(ctx.Request.Context(), workspaceID(ctx), request.TargetWorkspaceID)
if err != nil {
c.writeError(ctx, err)
return
}
ctx.JSON(http.StatusOK, cfg)
}
func (c *Controller) listProjects(ctx *gin.Context) {
projects, err := c.service.ListProjectsForWorkspace(ctx.Request.Context(), workspaceID(ctx))
if err != nil {
c.writeError(ctx, err)
return
}
ctx.JSON(http.StatusOK, gin.H{"projects": projects})
}
func (c *Controller) listRepositories(ctx *gin.Context) {
repositories, err := c.service.ListRepositoriesForWorkspace(
ctx.Request.Context(), workspaceID(ctx), ctx.Query("project"),
)
if err != nil {
c.writeError(ctx, err)
return
}
ctx.JSON(http.StatusOK, gin.H{"repositories": repositories})
}
func (c *Controller) listBranches(ctx *gin.Context) {
branches, err := c.service.ListBranchesForWorkspace(
ctx.Request.Context(), workspaceID(ctx), ctx.Query("organization"), ctx.Query("project"),
ctx.Query("repository"),
)
if err != nil {
c.writeError(ctx, err)
return
}
ctx.JSON(http.StatusOK, gin.H{"branches": branches})
}
func (c *Controller) searchWorkItems(ctx *gin.Context) {
var request struct {
Project string `json:"project"`
WIQL string `json:"wiql"`
Top int `json:"top"`
}
if err := ctx.ShouldBindJSON(&request); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
return
}
result, err := c.service.SearchWorkItemsForWorkspace(
ctx.Request.Context(), workspaceID(ctx), request.Project, request.WIQL, request.Top,
)
if err != nil {
c.writeError(ctx, err)
return
}
ctx.JSON(http.StatusOK, result)
}
func (c *Controller) getWorkItem(ctx *gin.Context) {
id, err := strconv.Atoi(ctx.Param("id"))
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid work item id"})
return
}
item, err := c.service.GetWorkItemForWorkspace(
ctx.Request.Context(), workspaceID(ctx), ctx.Query("project"), id,
)
if err != nil {
c.writeError(ctx, err)
return
}
ctx.JSON(http.StatusOK, item)
}
func (c *Controller) listPullRequests(ctx *gin.Context) {
skip, _ := strconv.Atoi(ctx.Query("skip"))
top, _ := strconv.Atoi(ctx.Query("top"))
result, err := c.service.ListPullRequestsForWorkspace(ctx.Request.Context(), workspaceID(ctx), PullRequestFilter{
ProjectID: ctx.Query("project"), RepositoryID: ctx.Query("repository"), Status: ctx.Query("status"),
CreatorID: ctx.Query("creator"), ReviewerID: ctx.Query("reviewer"),
SourceBranch: ctx.Query("source_branch"), TargetBranch: ctx.Query("target_branch"), Skip: skip, Top: top,
})
if err != nil {
c.writeError(ctx, err)
return
}
ctx.JSON(http.StatusOK, result)
}
func (c *Controller) getPullRequest(ctx *gin.Context) {
id, ok := pullRequestID(ctx)
if !ok {
return
}
pr, err := c.service.GetPullRequestForWorkspace(ctx.Request.Context(), workspaceID(ctx), ctx.Param("projectId"), ctx.Param("repositoryId"), id)
if err != nil {
c.writeError(ctx, err)
return
}
ctx.JSON(http.StatusOK, pr)
}
func (c *Controller) getPullRequestFeedback(ctx *gin.Context) {
id, ok := pullRequestID(ctx)
if !ok {
return
}
feedback, err := c.service.GetPullRequestFeedbackForWorkspace(ctx.Request.Context(), workspaceID(ctx), ctx.Param("projectId"), ctx.Param("repositoryId"), id)
if err != nil {
c.writeError(ctx, err)
return
}
ctx.JSON(http.StatusOK, feedback)
}
type taskPRRequest struct {
RepositoryID string `json:"repositoryId" binding:"required"`
PullRequestID int `json:"pullRequestId" binding:"required"`
}
func (c *Controller) listWorkspaceTaskPRs(ctx *gin.Context) {
rows, err := c.service.ListTaskPRsByWorkspace(ctx.Request.Context(), ctx.Param("workspaceId"))
if err != nil {
c.writeError(ctx, err)
return
}
ctx.JSON(http.StatusOK, TaskPRsResponse{TaskPRs: rows})
}
func (c *Controller) associateTaskPR(ctx *gin.Context) {
c.writeTaskPRSync(ctx, c.service.AssociateTaskPR)
}
func (c *Controller) syncTaskPR(ctx *gin.Context) {
c.writeTaskPRSync(ctx, c.service.SyncTaskPR)
}
func (c *Controller) writeTaskPRSync(
ctx *gin.Context,
sync func(context.Context, string, string, string, int) (*TaskPR, error),
) {
var request taskPRRequest
if err := ctx.ShouldBindJSON(&request); err != nil || request.PullRequestID <= 0 {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "repositoryId and positive pullRequestId are required"})
return
}
row, err := sync(
ctx.Request.Context(), workspaceID(ctx), ctx.Param("taskId"),
request.RepositoryID, request.PullRequestID,
)
if err != nil {
c.writeError(ctx, err)
return
}
ctx.JSON(http.StatusOK, row)
}
func (c *Controller) writeError(ctx *gin.Context, err error) {
status := http.StatusInternalServerError
code := ""
switch {
case errors.Is(err, ErrInvalidWorkspaceID), errors.Is(err, ErrInvalidConfig),
errors.Is(err, ErrInvalidTaskPRAssociation):
status = http.StatusBadRequest
case errors.Is(err, ErrNotConfigured):
status, code = http.StatusServiceUnavailable, notConfiguredCode
default:
var apiErr *APIError
if errors.As(err, &apiErr) {
status = upstreamStatus(apiErr.StatusCode)
}
}
body := gin.H{"error": err.Error()}
if code != "" {
body["code"] = code
}
ctx.JSON(status, body)
}
func workspaceID(ctx *gin.Context) string { return strings.TrimSpace(ctx.Query("workspace_id")) }
func pullRequestID(ctx *gin.Context) (int, bool) {
id, err := strconv.Atoi(ctx.Param("pullRequestId"))
if err != nil || id <= 0 {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid pull request id"})
return 0, false
}
return id, true
}
func upstreamStatus(status int) int {
switch status {
case http.StatusUnauthorized, http.StatusForbidden, http.StatusTooManyRequests:
return status
default:
if status >= 500 {
return http.StatusBadGateway
}
return http.StatusBadRequest
}
}