feat: added workspaces
This commit is contained in:
@@ -139,7 +139,7 @@ test-backend:
|
||||
.PHONY: test-web
|
||||
test-web:
|
||||
@echo "Running web app tests..."
|
||||
@cd $(WEB_DIR) && $(NPM) test --passWithNoTests || true
|
||||
@cd $(WEB_DIR) && $(NPM) test
|
||||
|
||||
#
|
||||
# Code Quality
|
||||
|
||||
@@ -202,6 +202,7 @@ func main() {
|
||||
gateway := gateways.NewGateway(log)
|
||||
|
||||
// Prepare Task Service controllers for HTTP + WebSocket handlers
|
||||
workspaceController := taskcontroller.NewWorkspaceController(taskSvc)
|
||||
boardController := taskcontroller.NewBoardController(taskSvc)
|
||||
taskController := taskcontroller.NewTaskController(taskSvc)
|
||||
|
||||
@@ -314,6 +315,7 @@ func main() {
|
||||
gateway.SetupRoutes(router)
|
||||
|
||||
// Task Service handlers (HTTP + WebSocket)
|
||||
taskhandlers.RegisterWorkspaceRoutes(router, gateway.Dispatcher, workspaceController, log)
|
||||
taskhandlers.RegisterBoardRoutes(router, gateway.Dispatcher, boardController, log)
|
||||
taskhandlers.RegisterTaskRoutes(router, gateway.Dispatcher, taskController, log)
|
||||
log.Info("Registered Task Service handlers (HTTP + WebSocket)")
|
||||
|
||||
@@ -75,8 +75,10 @@ func NewTestServer(t *testing.T) *TestServer {
|
||||
gateway.SetupRoutes(router)
|
||||
|
||||
// Register handlers (HTTP + WS)
|
||||
workspaceController := taskcontroller.NewWorkspaceController(taskSvc)
|
||||
boardController := taskcontroller.NewBoardController(taskSvc)
|
||||
taskController := taskcontroller.NewTaskController(taskSvc)
|
||||
taskhandlers.RegisterWorkspaceRoutes(router, gateway.Dispatcher, workspaceController, log)
|
||||
taskhandlers.RegisterBoardRoutes(router, gateway.Dispatcher, boardController, log)
|
||||
taskhandlers.RegisterTaskRoutes(router, gateway.Dispatcher, taskController, log)
|
||||
|
||||
@@ -139,6 +141,21 @@ func NewWSClient(t *testing.T, serverURL string) *WSClient {
|
||||
return client
|
||||
}
|
||||
|
||||
func createWorkspace(t *testing.T, client *WSClient) string {
|
||||
t.Helper()
|
||||
|
||||
resp, err := client.SendRequest("workspace-1", ws.ActionWorkspaceCreate, map[string]interface{}{
|
||||
"name": "Test Workspace",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var payload map[string]interface{}
|
||||
err = resp.ParsePayload(&payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
return payload["id"].(string)
|
||||
}
|
||||
|
||||
// readPump reads messages from the WebSocket connection
|
||||
func (c *WSClient) readPump() {
|
||||
defer close(c.done)
|
||||
@@ -262,11 +279,14 @@ func TestBoardCRUD(t *testing.T) {
|
||||
client := NewWSClient(t, ts.Server.URL)
|
||||
defer client.Close()
|
||||
|
||||
workspaceID := createWorkspace(t, client)
|
||||
|
||||
// Create board
|
||||
t.Run("CreateBoard", func(t *testing.T) {
|
||||
resp, err := client.SendRequest("board-create-1", ws.ActionBoardCreate, map[string]interface{}{
|
||||
"name": "Test Board",
|
||||
"description": "A test board for integration testing",
|
||||
"workspace_id": workspaceID,
|
||||
"name": "Test Board",
|
||||
"description": "A test board for integration testing",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, ws.MessageTypeResponse, resp.Type)
|
||||
@@ -281,7 +301,9 @@ func TestBoardCRUD(t *testing.T) {
|
||||
|
||||
// List boards
|
||||
t.Run("ListBoards", func(t *testing.T) {
|
||||
resp, err := client.SendRequest("board-list-1", ws.ActionBoardList, map[string]interface{}{})
|
||||
resp, err := client.SendRequest("board-list-1", ws.ActionBoardList, map[string]interface{}{
|
||||
"workspace_id": workspaceID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var payload map[string]interface{}
|
||||
@@ -305,9 +327,12 @@ func TestColumnCRUD(t *testing.T) {
|
||||
client := NewWSClient(t, ts.Server.URL)
|
||||
defer client.Close()
|
||||
|
||||
workspaceID := createWorkspace(t, client)
|
||||
|
||||
// First create a board
|
||||
boardResp, err := client.SendRequest("board-1", ws.ActionBoardCreate, map[string]interface{}{
|
||||
"name": "Column Test Board",
|
||||
"workspace_id": workspaceID,
|
||||
"name": "Column Test Board",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -378,9 +403,12 @@ func TestTaskCRUD(t *testing.T) {
|
||||
client := NewWSClient(t, ts.Server.URL)
|
||||
defer client.Close()
|
||||
|
||||
workspaceID := createWorkspace(t, client)
|
||||
|
||||
// Create board and column first
|
||||
boardResp, err := client.SendRequest("board-1", ws.ActionBoardCreate, map[string]interface{}{
|
||||
"name": "Task Test Board",
|
||||
"workspace_id": workspaceID,
|
||||
"name": "Task Test Board",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -406,11 +434,12 @@ func TestTaskCRUD(t *testing.T) {
|
||||
// Create task
|
||||
t.Run("CreateTask", func(t *testing.T) {
|
||||
resp, err := client.SendRequest("task-create-1", ws.ActionTaskCreate, map[string]interface{}{
|
||||
"board_id": boardID,
|
||||
"column_id": columnID,
|
||||
"title": "Test Task",
|
||||
"description": "A test task for integration testing",
|
||||
"priority": 3, // HIGH priority (1=LOW, 2=MEDIUM, 3=HIGH)
|
||||
"workspace_id": workspaceID,
|
||||
"board_id": boardID,
|
||||
"column_id": columnID,
|
||||
"title": "Test Task",
|
||||
"description": "A test task for integration testing",
|
||||
"priority": 3, // HIGH priority (1=LOW, 2=MEDIUM, 3=HIGH)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -519,9 +548,12 @@ func TestTaskStateTransitions(t *testing.T) {
|
||||
client := NewWSClient(t, ts.Server.URL)
|
||||
defer client.Close()
|
||||
|
||||
workspaceID := createWorkspace(t, client)
|
||||
|
||||
// Create board, column, and task
|
||||
boardResp, _ := client.SendRequest("board-1", ws.ActionBoardCreate, map[string]interface{}{
|
||||
"name": "State Test Board",
|
||||
"workspace_id": workspaceID,
|
||||
"name": "State Test Board",
|
||||
})
|
||||
var boardPayload map[string]interface{}
|
||||
boardResp.ParsePayload(&boardPayload)
|
||||
@@ -537,10 +569,11 @@ func TestTaskStateTransitions(t *testing.T) {
|
||||
columnID := colPayload["id"].(string)
|
||||
|
||||
taskResp, _ := client.SendRequest("task-1", ws.ActionTaskCreate, map[string]interface{}{
|
||||
"board_id": boardID,
|
||||
"column_id": columnID,
|
||||
"title": "State Test Task",
|
||||
"description": "Test state transitions",
|
||||
"workspace_id": workspaceID,
|
||||
"board_id": boardID,
|
||||
"column_id": columnID,
|
||||
"title": "State Test Task",
|
||||
"description": "Test state transitions",
|
||||
})
|
||||
var taskPayload map[string]interface{}
|
||||
taskResp.ParsePayload(&taskPayload)
|
||||
@@ -590,9 +623,12 @@ func TestTaskMove(t *testing.T) {
|
||||
client := NewWSClient(t, ts.Server.URL)
|
||||
defer client.Close()
|
||||
|
||||
workspaceID := createWorkspace(t, client)
|
||||
|
||||
// Create board with two columns
|
||||
boardResp, _ := client.SendRequest("board-1", ws.ActionBoardCreate, map[string]interface{}{
|
||||
"name": "Move Test Board",
|
||||
"workspace_id": workspaceID,
|
||||
"name": "Move Test Board",
|
||||
})
|
||||
var boardPayload map[string]interface{}
|
||||
boardResp.ParsePayload(&boardPayload)
|
||||
@@ -618,9 +654,10 @@ func TestTaskMove(t *testing.T) {
|
||||
|
||||
// Create task in first column
|
||||
taskResp, _ := client.SendRequest("task-1", ws.ActionTaskCreate, map[string]interface{}{
|
||||
"board_id": boardID,
|
||||
"column_id": col1ID,
|
||||
"title": "Movable Task",
|
||||
"workspace_id": workspaceID,
|
||||
"board_id": boardID,
|
||||
"column_id": col1ID,
|
||||
"title": "Movable Task",
|
||||
})
|
||||
var taskPayload map[string]interface{}
|
||||
taskResp.ParsePayload(&taskPayload)
|
||||
@@ -630,6 +667,7 @@ func TestTaskMove(t *testing.T) {
|
||||
t.Run("MoveToColumn2", func(t *testing.T) {
|
||||
resp, err := client.SendRequest("move-1", ws.ActionTaskMove, map[string]interface{}{
|
||||
"id": taskID,
|
||||
"board_id": boardID,
|
||||
"column_id": col2ID,
|
||||
"position": 0,
|
||||
})
|
||||
@@ -671,9 +709,12 @@ func TestMultipleClients(t *testing.T) {
|
||||
client2 := NewWSClient(t, ts.Server.URL)
|
||||
defer client2.Close()
|
||||
|
||||
workspaceID := createWorkspace(t, client1)
|
||||
|
||||
// Client 1 creates a board
|
||||
boardResp, err := client1.SendRequest("c1-board-1", ws.ActionBoardCreate, map[string]interface{}{
|
||||
"name": "Shared Board",
|
||||
"workspace_id": workspaceID,
|
||||
"name": "Shared Board",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -723,9 +764,10 @@ func TestMultipleClients(t *testing.T) {
|
||||
|
||||
// Client 1 creates a task in the column
|
||||
taskResp, err := client1.SendRequest("c1-task-1", ws.ActionTaskCreate, map[string]interface{}{
|
||||
"board_id": boardID,
|
||||
"column_id": columnID,
|
||||
"title": "Task by Client 1",
|
||||
"workspace_id": workspaceID,
|
||||
"board_id": boardID,
|
||||
"column_id": columnID,
|
||||
"title": "Task by Client 1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -821,9 +863,12 @@ func TestTaskSubscription(t *testing.T) {
|
||||
client := NewWSClient(t, ts.Server.URL)
|
||||
defer client.Close()
|
||||
|
||||
workspaceID := createWorkspace(t, client)
|
||||
|
||||
// Create a task first
|
||||
boardResp, _ := client.SendRequest("board-1", ws.ActionBoardCreate, map[string]interface{}{
|
||||
"name": "Sub Test Board",
|
||||
"workspace_id": workspaceID,
|
||||
"name": "Sub Test Board",
|
||||
})
|
||||
var boardPayload map[string]interface{}
|
||||
boardResp.ParsePayload(&boardPayload)
|
||||
@@ -839,9 +884,10 @@ func TestTaskSubscription(t *testing.T) {
|
||||
columnID := colPayload["id"].(string)
|
||||
|
||||
taskResp, _ := client.SendRequest("task-1", ws.ActionTaskCreate, map[string]interface{}{
|
||||
"board_id": boardID,
|
||||
"column_id": columnID,
|
||||
"title": "Subscribable Task",
|
||||
"workspace_id": workspaceID,
|
||||
"board_id": boardID,
|
||||
"column_id": columnID,
|
||||
"title": "Subscribable Task",
|
||||
})
|
||||
var taskPayload map[string]interface{}
|
||||
taskResp.ParsePayload(&taskPayload)
|
||||
@@ -1003,8 +1049,10 @@ func NewTestServerWithACP(t *testing.T) *TestServerWithACP {
|
||||
gateway.SetupRoutes(router)
|
||||
|
||||
// Register handlers (HTTP + WS)
|
||||
workspaceController := taskcontroller.NewWorkspaceController(taskSvc)
|
||||
boardController := taskcontroller.NewBoardController(taskSvc)
|
||||
taskController := taskcontroller.NewTaskController(taskSvc)
|
||||
taskhandlers.RegisterWorkspaceRoutes(router, gateway.Dispatcher, workspaceController, log)
|
||||
taskhandlers.RegisterBoardRoutes(router, gateway.Dispatcher, boardController, log)
|
||||
taskhandlers.RegisterTaskRoutes(router, gateway.Dispatcher, taskController, log)
|
||||
|
||||
@@ -1033,9 +1081,12 @@ func TestTaskLogsAction(t *testing.T) {
|
||||
client := NewWSClient(t, ts.Server.URL)
|
||||
defer client.Close()
|
||||
|
||||
workspaceID := createWorkspace(t, client)
|
||||
|
||||
// Create a task first
|
||||
boardResp, _ := client.SendRequest("board-1", ws.ActionBoardCreate, map[string]interface{}{
|
||||
"name": "Logs Test Board",
|
||||
"workspace_id": workspaceID,
|
||||
"name": "Logs Test Board",
|
||||
})
|
||||
var boardPayload map[string]interface{}
|
||||
boardResp.ParsePayload(&boardPayload)
|
||||
@@ -1051,9 +1102,10 @@ func TestTaskLogsAction(t *testing.T) {
|
||||
columnID := colPayload["id"].(string)
|
||||
|
||||
taskResp, _ := client.SendRequest("task-1", ws.ActionTaskCreate, map[string]interface{}{
|
||||
"board_id": boardID,
|
||||
"column_id": columnID,
|
||||
"title": "Task With Logs",
|
||||
"workspace_id": workspaceID,
|
||||
"board_id": boardID,
|
||||
"column_id": columnID,
|
||||
"title": "Task With Logs",
|
||||
})
|
||||
var taskPayload map[string]interface{}
|
||||
taskResp.ParsePayload(&taskPayload)
|
||||
@@ -1184,9 +1236,12 @@ func TestHistoricalLogsOnSubscribe(t *testing.T) {
|
||||
client := NewWSClient(t, ts.Server.URL)
|
||||
defer client.Close()
|
||||
|
||||
workspaceID := createWorkspace(t, client)
|
||||
|
||||
// Create a task
|
||||
boardResp, _ := client.SendRequest("board-1", ws.ActionBoardCreate, map[string]interface{}{
|
||||
"name": "Subscribe Test Board",
|
||||
"workspace_id": workspaceID,
|
||||
"name": "Subscribe Test Board",
|
||||
})
|
||||
var boardPayload map[string]interface{}
|
||||
boardResp.ParsePayload(&boardPayload)
|
||||
@@ -1202,9 +1257,10 @@ func TestHistoricalLogsOnSubscribe(t *testing.T) {
|
||||
columnID := colPayload["id"].(string)
|
||||
|
||||
taskResp, _ := client.SendRequest("task-1", ws.ActionTaskCreate, map[string]interface{}{
|
||||
"board_id": boardID,
|
||||
"column_id": columnID,
|
||||
"title": "Subscribable Task With History",
|
||||
"workspace_id": workspaceID,
|
||||
"board_id": boardID,
|
||||
"column_id": columnID,
|
||||
"title": "Subscribable Task With History",
|
||||
})
|
||||
var taskPayload map[string]interface{}
|
||||
taskResp.ParsePayload(&taskPayload)
|
||||
@@ -1317,9 +1373,12 @@ func TestHistoricalLogsChronologicalOrder(t *testing.T) {
|
||||
client := NewWSClient(t, ts.Server.URL)
|
||||
defer client.Close()
|
||||
|
||||
workspaceID := createWorkspace(t, client)
|
||||
|
||||
// Create a task
|
||||
boardResp, _ := client.SendRequest("board-1", ws.ActionBoardCreate, map[string]interface{}{
|
||||
"name": "Chrono Test Board",
|
||||
"workspace_id": workspaceID,
|
||||
"name": "Chrono Test Board",
|
||||
})
|
||||
var boardPayload map[string]interface{}
|
||||
boardResp.ParsePayload(&boardPayload)
|
||||
@@ -1335,9 +1394,10 @@ func TestHistoricalLogsChronologicalOrder(t *testing.T) {
|
||||
columnID := colPayload["id"].(string)
|
||||
|
||||
taskResp, _ := client.SendRequest("task-1", ws.ActionTaskCreate, map[string]interface{}{
|
||||
"board_id": boardID,
|
||||
"column_id": columnID,
|
||||
"title": "Chronological Order Test Task",
|
||||
"workspace_id": workspaceID,
|
||||
"board_id": boardID,
|
||||
"column_id": columnID,
|
||||
"title": "Chronological Order Test Task",
|
||||
})
|
||||
var taskPayload map[string]interface{}
|
||||
taskResp.ParsePayload(&taskPayload)
|
||||
@@ -1435,9 +1495,12 @@ func TestConcurrentRequests(t *testing.T) {
|
||||
client := NewWSClient(t, ts.Server.URL)
|
||||
defer client.Close()
|
||||
|
||||
workspaceID := createWorkspace(t, client)
|
||||
|
||||
// Create board and column
|
||||
boardResp, _ := client.SendRequest("board-1", ws.ActionBoardCreate, map[string]interface{}{
|
||||
"name": "Concurrent Test Board",
|
||||
"workspace_id": workspaceID,
|
||||
"name": "Concurrent Test Board",
|
||||
})
|
||||
var boardPayload map[string]interface{}
|
||||
boardResp.ParsePayload(&boardPayload)
|
||||
@@ -1470,9 +1533,10 @@ func TestConcurrentRequests(t *testing.T) {
|
||||
"task-"+string(rune('0'+idx)),
|
||||
ws.ActionTaskCreate,
|
||||
map[string]interface{}{
|
||||
"board_id": boardID,
|
||||
"column_id": columnID,
|
||||
"title": "Concurrent Task " + string(rune('0'+idx)),
|
||||
"workspace_id": workspaceID,
|
||||
"board_id": boardID,
|
||||
"column_id": columnID,
|
||||
"title": "Concurrent Task " + string(rune('0'+idx)),
|
||||
},
|
||||
)
|
||||
results <- err
|
||||
|
||||
@@ -497,8 +497,10 @@ func NewOrchestratorTestServer(t *testing.T) *OrchestratorTestServer {
|
||||
gateway.SetupRoutes(router)
|
||||
|
||||
// Register handlers (HTTP + WS)
|
||||
workspaceController := taskcontroller.NewWorkspaceController(taskSvc)
|
||||
boardController := taskcontroller.NewBoardController(taskSvc)
|
||||
taskController := taskcontroller.NewTaskController(taskSvc)
|
||||
taskhandlers.RegisterWorkspaceRoutes(router, gateway.Dispatcher, workspaceController, log)
|
||||
taskhandlers.RegisterBoardRoutes(router, gateway.Dispatcher, boardController, log)
|
||||
taskhandlers.RegisterTaskRoutes(router, gateway.Dispatcher, taskController, log)
|
||||
|
||||
@@ -600,6 +602,21 @@ func NewOrchestratorWSClient(t *testing.T, serverURL string) *OrchestratorWSClie
|
||||
return client
|
||||
}
|
||||
|
||||
func createOrchestratorWorkspace(t *testing.T, client *OrchestratorWSClient) string {
|
||||
t.Helper()
|
||||
|
||||
resp, err := client.SendRequest("workspace-1", ws.ActionWorkspaceCreate, map[string]interface{}{
|
||||
"name": "Test Workspace",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var payload map[string]interface{}
|
||||
err = resp.ParsePayload(&payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
return payload["id"].(string)
|
||||
}
|
||||
|
||||
// readPump reads messages from the WebSocket connection
|
||||
func (c *OrchestratorWSClient) readPump() {
|
||||
defer close(c.done)
|
||||
@@ -1258,10 +1275,13 @@ func TestOrchestratorEndToEndWorkflow(t *testing.T) {
|
||||
client := NewOrchestratorWSClient(t, ts.Server.URL)
|
||||
defer client.Close()
|
||||
|
||||
workspaceID := createOrchestratorWorkspace(t, client)
|
||||
|
||||
// 1. Create board
|
||||
boardResp, err := client.SendRequest("board-1", ws.ActionBoardCreate, map[string]interface{}{
|
||||
"name": "E2E Test Board",
|
||||
"description": "End-to-end test board",
|
||||
"workspace_id": workspaceID,
|
||||
"name": "E2E Test Board",
|
||||
"description": "End-to-end test board",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -1284,12 +1304,13 @@ func TestOrchestratorEndToEndWorkflow(t *testing.T) {
|
||||
|
||||
// 3. Create task with agent type
|
||||
taskResp, err := client.SendRequest("task-1", ws.ActionTaskCreate, map[string]interface{}{
|
||||
"board_id": boardID,
|
||||
"column_id": colID,
|
||||
"title": "Implement feature X",
|
||||
"description": "Create a new feature with tests",
|
||||
"priority": 2,
|
||||
"agent_type": "augment-agent",
|
||||
"workspace_id": workspaceID,
|
||||
"board_id": boardID,
|
||||
"column_id": colID,
|
||||
"title": "Implement feature X",
|
||||
"description": "Create a new feature with tests",
|
||||
"priority": 2,
|
||||
"agent_type": "augment-agent",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ func NewBoardController(svc *service.Service) *BoardController {
|
||||
return &BoardController{service: svc}
|
||||
}
|
||||
|
||||
func (c *BoardController) ListBoards(ctx context.Context, _ dto.ListBoardsRequest) (dto.ListBoardsResponse, error) {
|
||||
boards, err := c.service.ListBoards(ctx)
|
||||
func (c *BoardController) ListBoards(ctx context.Context, req dto.ListBoardsRequest) (dto.ListBoardsResponse, error) {
|
||||
boards, err := c.service.ListBoards(ctx, req.WorkspaceID)
|
||||
if err != nil {
|
||||
return dto.ListBoardsResponse{}, err
|
||||
}
|
||||
@@ -40,9 +40,9 @@ func (c *BoardController) GetBoard(ctx context.Context, req dto.GetBoardRequest)
|
||||
|
||||
func (c *BoardController) CreateBoard(ctx context.Context, req dto.CreateBoardRequest) (dto.BoardDTO, error) {
|
||||
board, err := c.service.CreateBoard(ctx, &service.CreateBoardRequest{
|
||||
WorkspaceID: req.WorkspaceID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
OwnerID: req.OwnerID,
|
||||
})
|
||||
if err != nil {
|
||||
return dto.BoardDTO{}, err
|
||||
|
||||
@@ -40,6 +40,7 @@ func (c *TaskController) GetTask(ctx context.Context, req dto.GetTaskRequest) (d
|
||||
|
||||
func (c *TaskController) CreateTask(ctx context.Context, req dto.CreateTaskRequest) (dto.TaskDTO, error) {
|
||||
task, err := c.service.CreateTask(ctx, &service.CreateTaskRequest{
|
||||
WorkspaceID: req.WorkspaceID,
|
||||
BoardID: req.BoardID,
|
||||
ColumnID: req.ColumnID,
|
||||
Title: req.Title,
|
||||
@@ -82,7 +83,7 @@ func (c *TaskController) DeleteTask(ctx context.Context, req dto.DeleteTaskReque
|
||||
}
|
||||
|
||||
func (c *TaskController) MoveTask(ctx context.Context, req dto.MoveTaskRequest) (dto.TaskDTO, error) {
|
||||
task, err := c.service.MoveTask(ctx, req.ID, req.ColumnID, req.Position)
|
||||
task, err := c.service.MoveTask(ctx, req.ID, req.BoardID, req.ColumnID, req.Position)
|
||||
if err != nil {
|
||||
return dto.TaskDTO{}, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/kandev/kandev/internal/task/dto"
|
||||
"github.com/kandev/kandev/internal/task/service"
|
||||
)
|
||||
|
||||
type WorkspaceController struct {
|
||||
service *service.Service
|
||||
}
|
||||
|
||||
func NewWorkspaceController(svc *service.Service) *WorkspaceController {
|
||||
return &WorkspaceController{service: svc}
|
||||
}
|
||||
|
||||
func (c *WorkspaceController) ListWorkspaces(ctx context.Context, _ dto.ListWorkspacesRequest) (dto.ListWorkspacesResponse, error) {
|
||||
workspaces, err := c.service.ListWorkspaces(ctx)
|
||||
if err != nil {
|
||||
return dto.ListWorkspacesResponse{}, err
|
||||
}
|
||||
resp := dto.ListWorkspacesResponse{
|
||||
Workspaces: make([]dto.WorkspaceDTO, 0, len(workspaces)),
|
||||
Total: len(workspaces),
|
||||
}
|
||||
for _, workspace := range workspaces {
|
||||
resp.Workspaces = append(resp.Workspaces, dto.FromWorkspace(workspace))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *WorkspaceController) GetWorkspace(ctx context.Context, req dto.GetWorkspaceRequest) (dto.WorkspaceDTO, error) {
|
||||
workspace, err := c.service.GetWorkspace(ctx, req.ID)
|
||||
if err != nil {
|
||||
return dto.WorkspaceDTO{}, err
|
||||
}
|
||||
return dto.FromWorkspace(workspace), nil
|
||||
}
|
||||
|
||||
func (c *WorkspaceController) CreateWorkspace(ctx context.Context, req dto.CreateWorkspaceRequest) (dto.WorkspaceDTO, error) {
|
||||
workspace, err := c.service.CreateWorkspace(ctx, &service.CreateWorkspaceRequest{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
OwnerID: req.OwnerID,
|
||||
})
|
||||
if err != nil {
|
||||
return dto.WorkspaceDTO{}, err
|
||||
}
|
||||
return dto.FromWorkspace(workspace), nil
|
||||
}
|
||||
|
||||
func (c *WorkspaceController) UpdateWorkspace(ctx context.Context, req dto.UpdateWorkspaceRequest) (dto.WorkspaceDTO, error) {
|
||||
workspace, err := c.service.UpdateWorkspace(ctx, req.ID, &service.UpdateWorkspaceRequest{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
})
|
||||
if err != nil {
|
||||
return dto.WorkspaceDTO{}, err
|
||||
}
|
||||
return dto.FromWorkspace(workspace), nil
|
||||
}
|
||||
|
||||
func (c *WorkspaceController) DeleteWorkspace(ctx context.Context, req dto.DeleteWorkspaceRequest) (dto.SuccessResponse, error) {
|
||||
if err := c.service.DeleteWorkspace(ctx, req.ID); err != nil {
|
||||
return dto.SuccessResponse{}, err
|
||||
}
|
||||
return dto.SuccessResponse{Success: true}, nil
|
||||
}
|
||||
@@ -8,6 +8,15 @@ import (
|
||||
)
|
||||
|
||||
type BoardDTO struct {
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type WorkspaceDTO struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
@@ -28,6 +37,7 @@ type ColumnDTO struct {
|
||||
|
||||
type TaskDTO struct {
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
BoardID string `json:"board_id"`
|
||||
ColumnID string `json:"column_id"`
|
||||
Title string `json:"title"`
|
||||
@@ -55,6 +65,11 @@ type ListBoardsResponse struct {
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
type ListWorkspacesResponse struct {
|
||||
Workspaces []WorkspaceDTO `json:"workspaces"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
type ListColumnsResponse struct {
|
||||
Columns []ColumnDTO `json:"columns"`
|
||||
Total int `json:"total"`
|
||||
@@ -77,14 +92,30 @@ func FromBoard(board *models.Board) BoardDTO {
|
||||
|
||||
return BoardDTO{
|
||||
ID: board.ID,
|
||||
WorkspaceID: board.WorkspaceID,
|
||||
Name: board.Name,
|
||||
Description: description,
|
||||
OwnerID: board.OwnerID,
|
||||
CreatedAt: board.CreatedAt,
|
||||
UpdatedAt: board.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func FromWorkspace(workspace *models.Workspace) WorkspaceDTO {
|
||||
var description *string
|
||||
if workspace.Description != "" {
|
||||
description = &workspace.Description
|
||||
}
|
||||
|
||||
return WorkspaceDTO{
|
||||
ID: workspace.ID,
|
||||
Name: workspace.Name,
|
||||
Description: description,
|
||||
OwnerID: workspace.OwnerID,
|
||||
CreatedAt: workspace.CreatedAt,
|
||||
UpdatedAt: workspace.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func FromColumn(column *models.Column) ColumnDTO {
|
||||
return ColumnDTO{
|
||||
ID: column.ID,
|
||||
@@ -117,6 +148,7 @@ func FromTask(task *models.Task) TaskDTO {
|
||||
|
||||
return TaskDTO{
|
||||
ID: task.ID,
|
||||
WorkspaceID: task.WorkspaceID,
|
||||
BoardID: task.BoardID,
|
||||
ColumnID: task.ColumnID,
|
||||
Title: task.Title,
|
||||
|
||||
@@ -2,16 +2,40 @@ package dto
|
||||
|
||||
import v1 "github.com/kandev/kandev/pkg/api/v1"
|
||||
|
||||
type ListBoardsRequest struct{}
|
||||
type ListBoardsRequest struct {
|
||||
WorkspaceID string
|
||||
}
|
||||
|
||||
type ListWorkspacesRequest struct{}
|
||||
|
||||
type GetWorkspaceRequest struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
type CreateWorkspaceRequest struct {
|
||||
Name string
|
||||
Description string
|
||||
OwnerID string
|
||||
}
|
||||
|
||||
type UpdateWorkspaceRequest struct {
|
||||
ID string
|
||||
Name *string
|
||||
Description *string
|
||||
}
|
||||
|
||||
type DeleteWorkspaceRequest struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
type GetBoardRequest struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
type CreateBoardRequest struct {
|
||||
WorkspaceID string
|
||||
Name string
|
||||
Description string
|
||||
OwnerID string
|
||||
}
|
||||
|
||||
type UpdateBoardRequest struct {
|
||||
@@ -52,6 +76,7 @@ type GetTaskRequest struct {
|
||||
}
|
||||
|
||||
type CreateTaskRequest struct {
|
||||
WorkspaceID string
|
||||
BoardID string
|
||||
ColumnID string
|
||||
Title string
|
||||
@@ -82,6 +107,7 @@ type DeleteTaskRequest struct {
|
||||
|
||||
type MoveTaskRequest struct {
|
||||
ID string
|
||||
BoardID string
|
||||
ColumnID string
|
||||
Position int
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ func RegisterBoardRoutes(router *gin.Engine, dispatcher *ws.Dispatcher, ctrl *co
|
||||
func (h *BoardHandlers) registerHTTP(router *gin.Engine) {
|
||||
api := router.Group("/api/v1")
|
||||
api.GET("/boards", h.httpListBoards)
|
||||
api.GET("/workspaces/:id/boards", h.httpListBoardsByWorkspace)
|
||||
api.GET("/boards/:id", h.httpGetBoard)
|
||||
api.GET("/boards/:id/columns", h.httpListColumns)
|
||||
api.GET("/boards/:id/snapshot", h.httpGetBoardSnapshot)
|
||||
@@ -54,7 +55,21 @@ func (h *BoardHandlers) registerWS(dispatcher *ws.Dispatcher) {
|
||||
// HTTP handlers
|
||||
|
||||
func (h *BoardHandlers) httpListBoards(c *gin.Context) {
|
||||
resp, err := h.controller.ListBoards(c.Request.Context(), dto.ListBoardsRequest{})
|
||||
resp, err := h.controller.ListBoards(c.Request.Context(), dto.ListBoardsRequest{
|
||||
WorkspaceID: c.Query("workspace_id"),
|
||||
})
|
||||
if err != nil {
|
||||
h.logger.Error("failed to list boards", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list boards"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (h *BoardHandlers) httpListBoardsByWorkspace(c *gin.Context) {
|
||||
resp, err := h.controller.ListBoards(c.Request.Context(), dto.ListBoardsRequest{
|
||||
WorkspaceID: c.Param("id"),
|
||||
})
|
||||
if err != nil {
|
||||
h.logger.Error("failed to list boards", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list boards"})
|
||||
@@ -93,7 +108,15 @@ func (h *BoardHandlers) httpGetBoardSnapshot(c *gin.Context) {
|
||||
// WS handlers
|
||||
|
||||
func (h *BoardHandlers) wsListBoards(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
|
||||
resp, err := h.controller.ListBoards(ctx, dto.ListBoardsRequest{})
|
||||
var req struct {
|
||||
WorkspaceID string `json:"workspace_id,omitempty"`
|
||||
}
|
||||
if msg.Payload != nil {
|
||||
if err := msg.ParsePayload(&req); err != nil {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil)
|
||||
}
|
||||
}
|
||||
resp, err := h.controller.ListBoards(ctx, dto.ListBoardsRequest{WorkspaceID: req.WorkspaceID})
|
||||
if err != nil {
|
||||
h.logger.Error("failed to list boards", zap.Error(err))
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to list boards", nil)
|
||||
@@ -102,6 +125,7 @@ func (h *BoardHandlers) wsListBoards(ctx context.Context, msg *ws.Message) (*ws.
|
||||
}
|
||||
|
||||
type wsCreateBoardRequest struct {
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
@@ -114,8 +138,12 @@ func (h *BoardHandlers) wsCreateBoard(ctx context.Context, msg *ws.Message) (*ws
|
||||
if req.Name == "" {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "name is required", nil)
|
||||
}
|
||||
if req.WorkspaceID == "" {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "workspace_id is required", nil)
|
||||
}
|
||||
|
||||
resp, err := h.controller.CreateBoard(ctx, dto.CreateBoardRequest{
|
||||
WorkspaceID: req.WorkspaceID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
})
|
||||
|
||||
@@ -72,6 +72,7 @@ func (h *TaskHandlers) httpGetTask(c *gin.Context) {
|
||||
}
|
||||
|
||||
type httpCreateTaskRequest struct {
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
BoardID string `json:"board_id"`
|
||||
ColumnID string `json:"column_id"`
|
||||
Title string `json:"title"`
|
||||
@@ -91,7 +92,12 @@ func (h *TaskHandlers) httpCreateTask(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
|
||||
return
|
||||
}
|
||||
if body.WorkspaceID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "workspace_id is required"})
|
||||
return
|
||||
}
|
||||
resp, err := h.controller.CreateTask(c.Request.Context(), dto.CreateTaskRequest{
|
||||
WorkspaceID: body.WorkspaceID,
|
||||
BoardID: body.BoardID,
|
||||
ColumnID: body.ColumnID,
|
||||
Title: body.Title,
|
||||
@@ -177,6 +183,7 @@ func (h *TaskHandlers) wsListTasks(ctx context.Context, msg *ws.Message) (*ws.Me
|
||||
}
|
||||
|
||||
type wsCreateTaskRequest struct {
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
BoardID string `json:"board_id"`
|
||||
ColumnID string `json:"column_id"`
|
||||
Title string `json:"title"`
|
||||
@@ -195,6 +202,9 @@ func (h *TaskHandlers) wsCreateTask(ctx context.Context, msg *ws.Message) (*ws.M
|
||||
if err := msg.ParsePayload(&req); err != nil {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil)
|
||||
}
|
||||
if req.WorkspaceID == "" {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "workspace_id is required", nil)
|
||||
}
|
||||
if req.BoardID == "" {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "board_id is required", nil)
|
||||
}
|
||||
@@ -206,6 +216,7 @@ func (h *TaskHandlers) wsCreateTask(ctx context.Context, msg *ws.Message) (*ws.M
|
||||
}
|
||||
|
||||
resp, err := h.controller.CreateTask(ctx, dto.CreateTaskRequest{
|
||||
WorkspaceID: req.WorkspaceID,
|
||||
BoardID: req.BoardID,
|
||||
ColumnID: req.ColumnID,
|
||||
Title: req.Title,
|
||||
@@ -305,6 +316,7 @@ func (h *TaskHandlers) wsDeleteTask(ctx context.Context, msg *ws.Message) (*ws.M
|
||||
|
||||
type wsMoveTaskRequest struct {
|
||||
ID string `json:"id"`
|
||||
BoardID string `json:"board_id"`
|
||||
ColumnID string `json:"column_id"`
|
||||
Position int `json:"position"`
|
||||
}
|
||||
@@ -317,12 +329,16 @@ func (h *TaskHandlers) wsMoveTask(ctx context.Context, msg *ws.Message) (*ws.Mes
|
||||
if req.ID == "" {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil)
|
||||
}
|
||||
if req.BoardID == "" {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "board_id is required", nil)
|
||||
}
|
||||
if req.ColumnID == "" {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "column_id is required", nil)
|
||||
}
|
||||
|
||||
resp, err := h.controller.MoveTask(ctx, dto.MoveTaskRequest{
|
||||
ID: req.ID,
|
||||
BoardID: req.BoardID,
|
||||
ColumnID: req.ColumnID,
|
||||
Position: req.Position,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/kandev/kandev/internal/common/logger"
|
||||
"github.com/kandev/kandev/internal/task/controller"
|
||||
"github.com/kandev/kandev/internal/task/dto"
|
||||
ws "github.com/kandev/kandev/pkg/websocket"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type WorkspaceHandlers struct {
|
||||
controller *controller.WorkspaceController
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
func NewWorkspaceHandlers(ctrl *controller.WorkspaceController, log *logger.Logger) *WorkspaceHandlers {
|
||||
return &WorkspaceHandlers{
|
||||
controller: ctrl,
|
||||
logger: log.WithFields(zap.String("component", "task-workspace-handlers")),
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterWorkspaceRoutes(router *gin.Engine, dispatcher *ws.Dispatcher, ctrl *controller.WorkspaceController, log *logger.Logger) {
|
||||
handlers := NewWorkspaceHandlers(ctrl, log)
|
||||
handlers.registerHTTP(router)
|
||||
handlers.registerWS(dispatcher)
|
||||
}
|
||||
|
||||
func (h *WorkspaceHandlers) registerHTTP(router *gin.Engine) {
|
||||
api := router.Group("/api/v1")
|
||||
api.GET("/workspaces", h.httpListWorkspaces)
|
||||
api.POST("/workspaces", h.httpCreateWorkspace)
|
||||
api.GET("/workspaces/:id", h.httpGetWorkspace)
|
||||
api.PATCH("/workspaces/:id", h.httpUpdateWorkspace)
|
||||
api.DELETE("/workspaces/:id", h.httpDeleteWorkspace)
|
||||
}
|
||||
|
||||
func (h *WorkspaceHandlers) registerWS(dispatcher *ws.Dispatcher) {
|
||||
dispatcher.RegisterFunc(ws.ActionWorkspaceList, h.wsListWorkspaces)
|
||||
dispatcher.RegisterFunc(ws.ActionWorkspaceCreate, h.wsCreateWorkspace)
|
||||
dispatcher.RegisterFunc(ws.ActionWorkspaceGet, h.wsGetWorkspace)
|
||||
dispatcher.RegisterFunc(ws.ActionWorkspaceUpdate, h.wsUpdateWorkspace)
|
||||
dispatcher.RegisterFunc(ws.ActionWorkspaceDelete, h.wsDeleteWorkspace)
|
||||
}
|
||||
|
||||
// HTTP handlers
|
||||
|
||||
func (h *WorkspaceHandlers) httpListWorkspaces(c *gin.Context) {
|
||||
resp, err := h.controller.ListWorkspaces(c.Request.Context(), dto.ListWorkspacesRequest{})
|
||||
if err != nil {
|
||||
h.logger.Error("failed to list workspaces", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list workspaces"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
type httpCreateWorkspaceRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
OwnerID string `json:"owner_id,omitempty"`
|
||||
}
|
||||
|
||||
func (h *WorkspaceHandlers) httpCreateWorkspace(c *gin.Context) {
|
||||
var body httpCreateWorkspaceRequest
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
|
||||
return
|
||||
}
|
||||
if body.Name == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
|
||||
return
|
||||
}
|
||||
resp, err := h.controller.CreateWorkspace(c.Request.Context(), dto.CreateWorkspaceRequest{
|
||||
Name: body.Name,
|
||||
Description: body.Description,
|
||||
OwnerID: body.OwnerID,
|
||||
})
|
||||
if err != nil {
|
||||
handleNotFound(c, h.logger, err, "workspace not created")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (h *WorkspaceHandlers) httpGetWorkspace(c *gin.Context) {
|
||||
resp, err := h.controller.GetWorkspace(c.Request.Context(), dto.GetWorkspaceRequest{ID: c.Param("id")})
|
||||
if err != nil {
|
||||
handleNotFound(c, h.logger, err, "workspace not found")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
type httpUpdateWorkspaceRequest struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
func (h *WorkspaceHandlers) httpUpdateWorkspace(c *gin.Context) {
|
||||
var body httpUpdateWorkspaceRequest
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
|
||||
return
|
||||
}
|
||||
resp, err := h.controller.UpdateWorkspace(c.Request.Context(), dto.UpdateWorkspaceRequest{
|
||||
ID: c.Param("id"),
|
||||
Name: body.Name,
|
||||
Description: body.Description,
|
||||
})
|
||||
if err != nil {
|
||||
handleNotFound(c, h.logger, err, "workspace not updated")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (h *WorkspaceHandlers) httpDeleteWorkspace(c *gin.Context) {
|
||||
resp, err := h.controller.DeleteWorkspace(c.Request.Context(), dto.DeleteWorkspaceRequest{ID: c.Param("id")})
|
||||
if err != nil {
|
||||
handleNotFound(c, h.logger, err, "workspace not deleted")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// WS handlers
|
||||
|
||||
func (h *WorkspaceHandlers) wsListWorkspaces(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
|
||||
resp, err := h.controller.ListWorkspaces(ctx, dto.ListWorkspacesRequest{})
|
||||
if err != nil {
|
||||
h.logger.Error("failed to list workspaces", zap.Error(err))
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to list workspaces", nil)
|
||||
}
|
||||
return ws.NewResponse(msg.ID, msg.Action, resp)
|
||||
}
|
||||
|
||||
type wsCreateWorkspaceRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
OwnerID string `json:"owner_id,omitempty"`
|
||||
}
|
||||
|
||||
func (h *WorkspaceHandlers) wsCreateWorkspace(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
|
||||
var req wsCreateWorkspaceRequest
|
||||
if err := msg.ParsePayload(&req); err != nil {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil)
|
||||
}
|
||||
if req.Name == "" {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "name is required", nil)
|
||||
}
|
||||
|
||||
resp, err := h.controller.CreateWorkspace(ctx, dto.CreateWorkspaceRequest{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
OwnerID: req.OwnerID,
|
||||
})
|
||||
if err != nil {
|
||||
h.logger.Error("failed to create workspace", zap.Error(err))
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to create workspace", nil)
|
||||
}
|
||||
return ws.NewResponse(msg.ID, msg.Action, resp)
|
||||
}
|
||||
|
||||
type wsGetWorkspaceRequest struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (h *WorkspaceHandlers) wsGetWorkspace(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
|
||||
var req wsGetWorkspaceRequest
|
||||
if err := msg.ParsePayload(&req); err != nil {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil)
|
||||
}
|
||||
if req.ID == "" {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil)
|
||||
}
|
||||
|
||||
resp, err := h.controller.GetWorkspace(ctx, dto.GetWorkspaceRequest{ID: req.ID})
|
||||
if err != nil {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeNotFound, "Workspace not found", nil)
|
||||
}
|
||||
return ws.NewResponse(msg.ID, msg.Action, resp)
|
||||
}
|
||||
|
||||
type wsUpdateWorkspaceRequest struct {
|
||||
ID string `json:"id"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
func (h *WorkspaceHandlers) wsUpdateWorkspace(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
|
||||
var req wsUpdateWorkspaceRequest
|
||||
if err := msg.ParsePayload(&req); err != nil {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil)
|
||||
}
|
||||
if req.ID == "" {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil)
|
||||
}
|
||||
|
||||
resp, err := h.controller.UpdateWorkspace(ctx, dto.UpdateWorkspaceRequest{
|
||||
ID: req.ID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
})
|
||||
if err != nil {
|
||||
h.logger.Error("failed to update workspace", zap.Error(err))
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to update workspace", nil)
|
||||
}
|
||||
return ws.NewResponse(msg.ID, msg.Action, resp)
|
||||
}
|
||||
|
||||
type wsDeleteWorkspaceRequest struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (h *WorkspaceHandlers) wsDeleteWorkspace(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
|
||||
var req wsDeleteWorkspaceRequest
|
||||
if err := msg.ParsePayload(&req); err != nil {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeBadRequest, "Invalid payload: "+err.Error(), nil)
|
||||
}
|
||||
if req.ID == "" {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "id is required", nil)
|
||||
}
|
||||
|
||||
resp, err := h.controller.DeleteWorkspace(ctx, dto.DeleteWorkspaceRequest{ID: req.ID})
|
||||
if err != nil {
|
||||
h.logger.Error("failed to delete workspace", zap.Error(err))
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to delete workspace", nil)
|
||||
}
|
||||
return ws.NewResponse(msg.ID, msg.Action, resp)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
// Task represents a task in the database
|
||||
type Task struct {
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
BoardID string `json:"board_id"`
|
||||
ColumnID string `json:"column_id"`
|
||||
Title string `json:"title"`
|
||||
@@ -27,6 +28,16 @@ type Task struct {
|
||||
|
||||
// Board represents a Kanban board
|
||||
type Board struct {
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Workspace represents a workspace
|
||||
type Workspace struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
@@ -70,6 +81,7 @@ func (t *Task) ToAPI() *v1.Task {
|
||||
|
||||
return &v1.Task{
|
||||
ID: t.ID,
|
||||
WorkspaceID: t.WorkspaceID,
|
||||
BoardID: t.BoardID,
|
||||
Title: t.Title,
|
||||
Description: t.Description,
|
||||
@@ -84,4 +96,3 @@ func (t *Task) ToAPI() *v1.Task {
|
||||
Metadata: t.Metadata,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ func TestTaskStructInitialization(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
task := Task{
|
||||
ID: "task-123",
|
||||
WorkspaceID: "workspace-001",
|
||||
BoardID: "board-456",
|
||||
ColumnID: "column-789",
|
||||
Title: "Test Task",
|
||||
@@ -51,6 +52,9 @@ func TestTaskStructInitialization(t *testing.T) {
|
||||
if task.ID != "task-123" {
|
||||
t.Errorf("expected ID task-123, got %s", task.ID)
|
||||
}
|
||||
if task.WorkspaceID != "workspace-001" {
|
||||
t.Errorf("expected WorkspaceID workspace-001, got %s", task.WorkspaceID)
|
||||
}
|
||||
if task.BoardID != "board-456" {
|
||||
t.Errorf("expected BoardID board-456, got %s", task.BoardID)
|
||||
}
|
||||
@@ -87,9 +91,9 @@ func TestBoardStructInitialization(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
board := Board{
|
||||
ID: "board-123",
|
||||
WorkspaceID: "workspace-001",
|
||||
Name: "Test Board",
|
||||
Description: "A test board",
|
||||
OwnerID: "owner-456",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
@@ -97,15 +101,15 @@ func TestBoardStructInitialization(t *testing.T) {
|
||||
if board.ID != "board-123" {
|
||||
t.Errorf("expected ID board-123, got %s", board.ID)
|
||||
}
|
||||
if board.WorkspaceID != "workspace-001" {
|
||||
t.Errorf("expected WorkspaceID workspace-001, got %s", board.WorkspaceID)
|
||||
}
|
||||
if board.Name != "Test Board" {
|
||||
t.Errorf("expected Name 'Test Board', got %s", board.Name)
|
||||
}
|
||||
if board.Description != "A test board" {
|
||||
t.Errorf("expected Description 'A test board', got %s", board.Description)
|
||||
}
|
||||
if board.OwnerID != "owner-456" {
|
||||
t.Errorf("expected OwnerID 'owner-456', got %s", board.OwnerID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestColumnStructInitialization(t *testing.T) {
|
||||
@@ -141,6 +145,7 @@ func TestTaskToAPI(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
task := &Task{
|
||||
ID: "task-123",
|
||||
WorkspaceID: "workspace-001",
|
||||
BoardID: "board-456",
|
||||
ColumnID: "column-789",
|
||||
Title: "Test Task",
|
||||
@@ -160,6 +165,9 @@ func TestTaskToAPI(t *testing.T) {
|
||||
if apiTask.ID != task.ID {
|
||||
t.Errorf("expected ID %s, got %s", task.ID, apiTask.ID)
|
||||
}
|
||||
if apiTask.WorkspaceID != task.WorkspaceID {
|
||||
t.Errorf("expected WorkspaceID %s, got %s", task.WorkspaceID, apiTask.WorkspaceID)
|
||||
}
|
||||
if apiTask.BoardID != task.BoardID {
|
||||
t.Errorf("expected BoardID %s, got %s", task.BoardID, apiTask.BoardID)
|
||||
}
|
||||
@@ -190,6 +198,7 @@ func TestTaskToAPIWithEmptyOptionalFields(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
task := &Task{
|
||||
ID: "task-123",
|
||||
WorkspaceID: "workspace-001",
|
||||
BoardID: "board-456",
|
||||
ColumnID: "column-789",
|
||||
Title: "Test Task",
|
||||
@@ -213,4 +222,3 @@ func TestTaskToAPIWithEmptyOptionalFields(t *testing.T) {
|
||||
t.Errorf("expected AssignedAgentID nil, got %v", apiTask.AssignedAgentID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,13 @@ import (
|
||||
|
||||
// Repository defines the interface for task storage operations
|
||||
type Repository interface {
|
||||
// Workspace operations
|
||||
CreateWorkspace(ctx context.Context, workspace *models.Workspace) error
|
||||
GetWorkspace(ctx context.Context, id string) (*models.Workspace, error)
|
||||
UpdateWorkspace(ctx context.Context, workspace *models.Workspace) error
|
||||
DeleteWorkspace(ctx context.Context, id string) error
|
||||
ListWorkspaces(ctx context.Context) ([]*models.Workspace, error)
|
||||
|
||||
// Task operations
|
||||
CreateTask(ctx context.Context, task *models.Task) error
|
||||
GetTask(ctx context.Context, id string) (*models.Task, error)
|
||||
@@ -17,13 +24,15 @@ type Repository interface {
|
||||
ListTasks(ctx context.Context, boardID string) ([]*models.Task, error)
|
||||
ListTasksByColumn(ctx context.Context, columnID string) ([]*models.Task, error)
|
||||
UpdateTaskState(ctx context.Context, id string, state v1.TaskState) error
|
||||
AddTaskToBoard(ctx context.Context, taskID, boardID, columnID string, position int) error
|
||||
RemoveTaskFromBoard(ctx context.Context, taskID, boardID string) error
|
||||
|
||||
// Board operations
|
||||
CreateBoard(ctx context.Context, board *models.Board) error
|
||||
GetBoard(ctx context.Context, id string) (*models.Board, error)
|
||||
UpdateBoard(ctx context.Context, board *models.Board) error
|
||||
DeleteBoard(ctx context.Context, id string) error
|
||||
ListBoards(ctx context.Context) ([]*models.Board, error)
|
||||
ListBoards(ctx context.Context, workspaceID string) ([]*models.Board, error)
|
||||
|
||||
// Column operations
|
||||
CreateColumn(ctx context.Context, column *models.Column) error
|
||||
@@ -35,4 +44,3 @@ type Repository interface {
|
||||
// Close closes the repository (for database connections)
|
||||
Close() error
|
||||
}
|
||||
|
||||
|
||||
@@ -13,10 +13,13 @@ import (
|
||||
|
||||
// MemoryRepository provides in-memory task storage operations
|
||||
type MemoryRepository struct {
|
||||
tasks map[string]*models.Task
|
||||
boards map[string]*models.Board
|
||||
columns map[string]*models.Column
|
||||
mu sync.RWMutex
|
||||
workspaces map[string]*models.Workspace
|
||||
tasks map[string]*models.Task
|
||||
boards map[string]*models.Board
|
||||
columns map[string]*models.Column
|
||||
taskBoards map[string]map[string]struct{}
|
||||
taskPlacements map[string]map[string]*taskPlacement
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Ensure MemoryRepository implements Repository interface
|
||||
@@ -25,17 +28,93 @@ var _ Repository = (*MemoryRepository)(nil)
|
||||
// NewMemoryRepository creates a new in-memory task repository
|
||||
func NewMemoryRepository() *MemoryRepository {
|
||||
return &MemoryRepository{
|
||||
tasks: make(map[string]*models.Task),
|
||||
boards: make(map[string]*models.Board),
|
||||
columns: make(map[string]*models.Column),
|
||||
workspaces: make(map[string]*models.Workspace),
|
||||
tasks: make(map[string]*models.Task),
|
||||
boards: make(map[string]*models.Board),
|
||||
columns: make(map[string]*models.Column),
|
||||
taskBoards: make(map[string]map[string]struct{}),
|
||||
taskPlacements: make(map[string]map[string]*taskPlacement),
|
||||
}
|
||||
}
|
||||
|
||||
type taskPlacement struct {
|
||||
boardID string
|
||||
columnID string
|
||||
position int
|
||||
}
|
||||
|
||||
// Close is a no-op for in-memory repository
|
||||
func (r *MemoryRepository) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Workspace operations
|
||||
|
||||
// CreateWorkspace creates a new workspace
|
||||
func (r *MemoryRepository) CreateWorkspace(ctx context.Context, workspace *models.Workspace) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if workspace.ID == "" {
|
||||
workspace.ID = uuid.New().String()
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
workspace.CreatedAt = now
|
||||
workspace.UpdatedAt = now
|
||||
|
||||
r.workspaces[workspace.ID] = workspace
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetWorkspace retrieves a workspace by ID
|
||||
func (r *MemoryRepository) GetWorkspace(ctx context.Context, id string) (*models.Workspace, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
workspace, ok := r.workspaces[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("workspace not found: %s", id)
|
||||
}
|
||||
return workspace, nil
|
||||
}
|
||||
|
||||
// UpdateWorkspace updates an existing workspace
|
||||
func (r *MemoryRepository) UpdateWorkspace(ctx context.Context, workspace *models.Workspace) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if _, ok := r.workspaces[workspace.ID]; !ok {
|
||||
return fmt.Errorf("workspace not found: %s", workspace.ID)
|
||||
}
|
||||
workspace.UpdatedAt = time.Now().UTC()
|
||||
r.workspaces[workspace.ID] = workspace
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteWorkspace deletes a workspace by ID
|
||||
func (r *MemoryRepository) DeleteWorkspace(ctx context.Context, id string) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if _, ok := r.workspaces[id]; !ok {
|
||||
return fmt.Errorf("workspace not found: %s", id)
|
||||
}
|
||||
delete(r.workspaces, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListWorkspaces returns all workspaces
|
||||
func (r *MemoryRepository) ListWorkspaces(ctx context.Context) ([]*models.Workspace, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
result := make([]*models.Workspace, 0, len(r.workspaces))
|
||||
for _, workspace := range r.workspaces {
|
||||
result = append(result, workspace)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Task operations
|
||||
|
||||
// CreateTask creates a new task
|
||||
@@ -51,6 +130,9 @@ func (r *MemoryRepository) CreateTask(ctx context.Context, task *models.Task) er
|
||||
task.UpdatedAt = now
|
||||
|
||||
r.tasks[task.ID] = task
|
||||
if task.BoardID != "" {
|
||||
r.addTaskPlacement(task.ID, task.BoardID, task.ColumnID, task.Position)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -76,6 +158,9 @@ func (r *MemoryRepository) UpdateTask(ctx context.Context, task *models.Task) er
|
||||
}
|
||||
task.UpdatedAt = time.Now().UTC()
|
||||
r.tasks[task.ID] = task
|
||||
if task.BoardID != "" {
|
||||
r.addTaskPlacement(task.ID, task.BoardID, task.ColumnID, task.Position)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -88,6 +173,8 @@ func (r *MemoryRepository) DeleteTask(ctx context.Context, id string) error {
|
||||
return fmt.Errorf("task not found: %s", id)
|
||||
}
|
||||
delete(r.tasks, id)
|
||||
delete(r.taskBoards, id)
|
||||
delete(r.taskPlacements, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -98,9 +185,15 @@ func (r *MemoryRepository) ListTasks(ctx context.Context, boardID string) ([]*mo
|
||||
|
||||
var result []*models.Task
|
||||
for _, task := range r.tasks {
|
||||
if task.BoardID == boardID {
|
||||
result = append(result, task)
|
||||
placement, ok := r.getTaskPlacement(task.ID, boardID)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
copy := *task
|
||||
copy.BoardID = placement.boardID
|
||||
copy.ColumnID = placement.columnID
|
||||
copy.Position = placement.position
|
||||
result = append(result, ©)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -112,8 +205,19 @@ func (r *MemoryRepository) ListTasksByColumn(ctx context.Context, columnID strin
|
||||
|
||||
var result []*models.Task
|
||||
for _, task := range r.tasks {
|
||||
if task.ColumnID == columnID {
|
||||
result = append(result, task)
|
||||
placements, ok := r.taskPlacements[task.ID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, placement := range placements {
|
||||
if placement.columnID != columnID {
|
||||
continue
|
||||
}
|
||||
copy := *task
|
||||
copy.BoardID = placement.boardID
|
||||
copy.ColumnID = placement.columnID
|
||||
copy.Position = placement.position
|
||||
result = append(result, ©)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
@@ -133,6 +237,35 @@ func (r *MemoryRepository) UpdateTaskState(ctx context.Context, id string, state
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddTaskToBoard adds a task to a board with placement
|
||||
func (r *MemoryRepository) AddTaskToBoard(ctx context.Context, taskID, boardID, columnID string, position int) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if _, ok := r.tasks[taskID]; !ok {
|
||||
return fmt.Errorf("task not found: %s", taskID)
|
||||
}
|
||||
r.addTaskPlacement(taskID, boardID, columnID, position)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveTaskFromBoard removes a task from a board
|
||||
func (r *MemoryRepository) RemoveTaskFromBoard(ctx context.Context, taskID, boardID string) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if _, ok := r.tasks[taskID]; !ok {
|
||||
return fmt.Errorf("task not found: %s", taskID)
|
||||
}
|
||||
if placements, ok := r.taskPlacements[taskID]; ok {
|
||||
delete(placements, boardID)
|
||||
}
|
||||
if boards, ok := r.taskBoards[taskID]; ok {
|
||||
delete(boards, boardID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Board operations
|
||||
|
||||
// CreateBoard creates a new board
|
||||
@@ -189,12 +322,15 @@ func (r *MemoryRepository) DeleteBoard(ctx context.Context, id string) error {
|
||||
}
|
||||
|
||||
// ListBoards returns all boards
|
||||
func (r *MemoryRepository) ListBoards(ctx context.Context) ([]*models.Board, error) {
|
||||
func (r *MemoryRepository) ListBoards(ctx context.Context, workspaceID string) ([]*models.Board, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
result := make([]*models.Board, 0, len(r.boards))
|
||||
for _, board := range r.boards {
|
||||
if workspaceID != "" && board.WorkspaceID != workspaceID {
|
||||
continue
|
||||
}
|
||||
result = append(result, board)
|
||||
}
|
||||
return result, nil
|
||||
@@ -269,3 +405,27 @@ func (r *MemoryRepository) ListColumns(ctx context.Context, boardID string) ([]*
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) addTaskPlacement(taskID, boardID, columnID string, position int) {
|
||||
if _, ok := r.taskBoards[taskID]; !ok {
|
||||
r.taskBoards[taskID] = make(map[string]struct{})
|
||||
}
|
||||
r.taskBoards[taskID][boardID] = struct{}{}
|
||||
|
||||
if _, ok := r.taskPlacements[taskID]; !ok {
|
||||
r.taskPlacements[taskID] = make(map[string]*taskPlacement)
|
||||
}
|
||||
r.taskPlacements[taskID][boardID] = &taskPlacement{
|
||||
boardID: boardID,
|
||||
columnID: columnID,
|
||||
position: position,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) getTaskPlacement(taskID, boardID string) (*taskPlacement, bool) {
|
||||
placements, ok := r.taskPlacements[taskID]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
placement, ok := placements[boardID]
|
||||
return placement, ok
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ func TestNewMemoryRepository(t *testing.T) {
|
||||
if repo.tasks == nil {
|
||||
t.Error("expected tasks map to be initialized")
|
||||
}
|
||||
if repo.workspaces == nil {
|
||||
t.Error("expected workspaces map to be initialized")
|
||||
}
|
||||
if repo.boards == nil {
|
||||
t.Error("expected boards map to be initialized")
|
||||
}
|
||||
@@ -38,8 +41,13 @@ func TestMemoryRepository_BoardCRUD(t *testing.T) {
|
||||
repo := NewMemoryRepository()
|
||||
ctx := context.Background()
|
||||
|
||||
workspace := &models.Workspace{ID: "ws-1", Name: "Workspace"}
|
||||
if err := repo.CreateWorkspace(ctx, workspace); err != nil {
|
||||
t.Fatalf("failed to create workspace: %v", err)
|
||||
}
|
||||
|
||||
// Create
|
||||
board := &models.Board{Name: "Test Board", Description: "A test board", OwnerID: "owner-123"}
|
||||
board := &models.Board{WorkspaceID: workspace.ID, Name: "Test Board", Description: "A test board"}
|
||||
if err := repo.CreateBoard(ctx, board); err != nil {
|
||||
t.Fatalf("failed to create board: %v", err)
|
||||
}
|
||||
@@ -103,16 +111,19 @@ func TestMemoryRepository_ListBoards(t *testing.T) {
|
||||
repo := NewMemoryRepository()
|
||||
ctx := context.Background()
|
||||
|
||||
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-1", Name: "Board 1"})
|
||||
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-2", Name: "Board 2"})
|
||||
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-3", Name: "Board 3"})
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace 1"})
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-2", Name: "Workspace 2"})
|
||||
|
||||
boards, err := repo.ListBoards(ctx)
|
||||
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-1", WorkspaceID: "ws-1", Name: "Board 1"})
|
||||
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-2", WorkspaceID: "ws-1", Name: "Board 2"})
|
||||
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-3", WorkspaceID: "ws-2", Name: "Board 3"})
|
||||
|
||||
boards, err := repo.ListBoards(ctx, "ws-1")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to list boards: %v", err)
|
||||
}
|
||||
if len(boards) != 3 {
|
||||
t.Errorf("expected 3 boards, got %d", len(boards))
|
||||
if len(boards) != 2 {
|
||||
t.Errorf("expected 2 boards, got %d", len(boards))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +217,7 @@ func TestMemoryRepository_TaskCRUD(t *testing.T) {
|
||||
|
||||
// Create
|
||||
task := &models.Task{
|
||||
WorkspaceID: "ws-1",
|
||||
BoardID: "board-123",
|
||||
ColumnID: "col-123",
|
||||
Title: "Test Task",
|
||||
@@ -277,7 +289,7 @@ func TestMemoryRepository_UpdateTaskState(t *testing.T) {
|
||||
repo := NewMemoryRepository()
|
||||
ctx := context.Background()
|
||||
|
||||
task := &models.Task{ID: "task-123", BoardID: "board-123", ColumnID: "col-123", Title: "Test", State: v1.TaskStateTODO}
|
||||
task := &models.Task{ID: "task-123", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-123", Title: "Test", State: v1.TaskStateTODO}
|
||||
_ = repo.CreateTask(ctx, task)
|
||||
|
||||
err := repo.UpdateTaskState(ctx, "task-123", v1.TaskStateInProgress)
|
||||
@@ -305,9 +317,9 @@ func TestMemoryRepository_ListTasks(t *testing.T) {
|
||||
repo := NewMemoryRepository()
|
||||
ctx := context.Background()
|
||||
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-1", BoardID: "board-123", ColumnID: "col-1", Title: "Task 1"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-2", BoardID: "board-123", ColumnID: "col-1", Title: "Task 2"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-3", BoardID: "board-456", ColumnID: "col-2", Title: "Task 3"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-1", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-1", Title: "Task 1"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-2", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-1", Title: "Task 2"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-3", WorkspaceID: "ws-1", BoardID: "board-456", ColumnID: "col-2", Title: "Task 3"})
|
||||
|
||||
tasks, err := repo.ListTasks(ctx, "board-123")
|
||||
if err != nil {
|
||||
@@ -322,9 +334,9 @@ func TestMemoryRepository_ListTasksByColumn(t *testing.T) {
|
||||
repo := NewMemoryRepository()
|
||||
ctx := context.Background()
|
||||
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-1", BoardID: "board-123", ColumnID: "col-1", Title: "Task 1"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-2", BoardID: "board-123", ColumnID: "col-1", Title: "Task 2"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-3", BoardID: "board-123", ColumnID: "col-2", Title: "Task 3"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-1", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-1", Title: "Task 1"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-2", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-1", Title: "Task 2"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-3", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-2", Title: "Task 3"})
|
||||
|
||||
tasks, err := repo.ListTasksByColumn(ctx, "col-1")
|
||||
if err != nil {
|
||||
@@ -334,4 +346,3 @@ func TestMemoryRepository_ListTasksByColumn(t *testing.T) {
|
||||
t.Errorf("expected 2 tasks for col-1, got %d", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ func NewSQLiteRepository(dbPath string) (*SQLiteRepository, error) {
|
||||
// initSchema creates the database tables if they don't exist
|
||||
func (r *SQLiteRepository) initSchema() error {
|
||||
schema := `
|
||||
CREATE TABLE IF NOT EXISTS boards (
|
||||
CREATE TABLE IF NOT EXISTS workspaces (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
@@ -56,6 +56,15 @@ func (r *SQLiteRepository) initSchema() error {
|
||||
updated_at DATETIME NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS boards (
|
||||
id TEXT PRIMARY KEY,
|
||||
workspace_id TEXT NOT NULL DEFAULT '',
|
||||
name TEXT NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS columns (
|
||||
id TEXT PRIMARY KEY,
|
||||
board_id TEXT NOT NULL,
|
||||
@@ -69,6 +78,7 @@ func (r *SQLiteRepository) initSchema() error {
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id TEXT PRIMARY KEY,
|
||||
workspace_id TEXT NOT NULL DEFAULT '',
|
||||
board_id TEXT NOT NULL,
|
||||
column_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
@@ -87,9 +97,33 @@ func (r *SQLiteRepository) initSchema() error {
|
||||
FOREIGN KEY (column_id) REFERENCES columns(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS task_boards (
|
||||
task_id TEXT NOT NULL,
|
||||
board_id TEXT NOT NULL,
|
||||
PRIMARY KEY (task_id, board_id),
|
||||
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS task_columns (
|
||||
task_id TEXT NOT NULL,
|
||||
board_id TEXT NOT NULL,
|
||||
column_id TEXT NOT NULL,
|
||||
position INTEGER DEFAULT 0,
|
||||
PRIMARY KEY (task_id, board_id),
|
||||
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (column_id) REFERENCES columns(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_board_id ON tasks(board_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_column_id ON tasks(column_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_workspace_id ON tasks(workspace_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_columns_board_id ON columns(board_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_boards_workspace_id ON boards(workspace_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_boards_board_id ON task_boards(board_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_columns_board_id ON task_columns(board_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_columns_column_id ON task_columns(column_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS task_agent_execution_logs (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -108,7 +142,123 @@ func (r *SQLiteRepository) initSchema() error {
|
||||
CREATE INDEX IF NOT EXISTS idx_execution_logs_task_timestamp ON task_agent_execution_logs(task_id, timestamp);
|
||||
`
|
||||
|
||||
_, err := r.db.Exec(schema)
|
||||
if _, err := r.db.Exec(schema); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.ensureColumn("boards", "workspace_id", "TEXT NOT NULL DEFAULT ''"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureColumn("tasks", "workspace_id", "TEXT NOT NULL DEFAULT ''"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.ensureDefaultWorkspace(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.backfillTaskMappings(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SQLiteRepository) ensureColumn(table, column, definition string) error {
|
||||
exists, err := r.columnExists(table, column)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
_, err = r.db.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", table, column, definition))
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *SQLiteRepository) columnExists(table, column string) (bool, error) {
|
||||
rows, err := r.db.Query(fmt.Sprintf("PRAGMA table_info(%s)", table))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var cid int
|
||||
var name, colType string
|
||||
var notNull int
|
||||
var defaultValue *string
|
||||
var pk int
|
||||
if err := rows.Scan(&cid, &name, &colType, ¬Null, &defaultValue, &pk); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if name == column {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, rows.Err()
|
||||
}
|
||||
|
||||
func (r *SQLiteRepository) ensureDefaultWorkspace() error {
|
||||
ctx := context.Background()
|
||||
|
||||
var count int
|
||||
if err := r.db.QueryRowContext(ctx, "SELECT COUNT(1) FROM workspaces").Scan(&count); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
defaultID := uuid.New().String()
|
||||
now := time.Now().UTC()
|
||||
if _, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO workspaces (id, name, description, owner_id, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`, defaultID, "Default Workspace", "", "", now, now); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var defaultWorkspaceID string
|
||||
if err := r.db.QueryRowContext(ctx, "SELECT id FROM workspaces ORDER BY created_at LIMIT 1").Scan(&defaultWorkspaceID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := r.db.ExecContext(ctx, `
|
||||
UPDATE boards SET workspace_id = ? WHERE workspace_id = '' OR workspace_id IS NULL
|
||||
`, defaultWorkspaceID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := r.db.ExecContext(ctx, `
|
||||
UPDATE tasks
|
||||
SET workspace_id = (
|
||||
SELECT workspace_id FROM boards WHERE boards.id = tasks.board_id
|
||||
)
|
||||
WHERE workspace_id = '' OR workspace_id IS NULL
|
||||
`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SQLiteRepository) backfillTaskMappings() error {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
INSERT OR IGNORE INTO task_boards (task_id, board_id)
|
||||
SELECT id, board_id FROM tasks WHERE board_id != ''
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = r.db.ExecContext(ctx, `
|
||||
INSERT OR IGNORE INTO task_columns (task_id, board_id, column_id, position)
|
||||
SELECT id, board_id, column_id, position
|
||||
FROM tasks
|
||||
WHERE board_id != '' AND column_id != ''
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -122,6 +272,93 @@ func (r *SQLiteRepository) DB() *sql.DB {
|
||||
return r.db
|
||||
}
|
||||
|
||||
// Workspace operations
|
||||
|
||||
// CreateWorkspace creates a new workspace
|
||||
func (r *SQLiteRepository) CreateWorkspace(ctx context.Context, workspace *models.Workspace) error {
|
||||
if workspace.ID == "" {
|
||||
workspace.ID = uuid.New().String()
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
workspace.CreatedAt = now
|
||||
workspace.UpdatedAt = now
|
||||
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO workspaces (id, name, description, owner_id, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`, workspace.ID, workspace.Name, workspace.Description, workspace.OwnerID, workspace.CreatedAt, workspace.UpdatedAt)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetWorkspace retrieves a workspace by ID
|
||||
func (r *SQLiteRepository) GetWorkspace(ctx context.Context, id string) (*models.Workspace, error) {
|
||||
workspace := &models.Workspace{}
|
||||
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT id, name, description, owner_id, created_at, updated_at
|
||||
FROM workspaces WHERE id = ?
|
||||
`, id).Scan(&workspace.ID, &workspace.Name, &workspace.Description, &workspace.OwnerID, &workspace.CreatedAt, &workspace.UpdatedAt)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("workspace not found: %s", id)
|
||||
}
|
||||
return workspace, err
|
||||
}
|
||||
|
||||
// UpdateWorkspace updates an existing workspace
|
||||
func (r *SQLiteRepository) UpdateWorkspace(ctx context.Context, workspace *models.Workspace) error {
|
||||
workspace.UpdatedAt = time.Now().UTC()
|
||||
|
||||
result, err := r.db.ExecContext(ctx, `
|
||||
UPDATE workspaces SET name = ?, description = ?, updated_at = ? WHERE id = ?
|
||||
`, workspace.Name, workspace.Description, workspace.UpdatedAt, workspace.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("workspace not found: %s", workspace.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteWorkspace deletes a workspace by ID
|
||||
func (r *SQLiteRepository) DeleteWorkspace(ctx context.Context, id string) error {
|
||||
result, err := r.db.ExecContext(ctx, `DELETE FROM workspaces WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("workspace not found: %s", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListWorkspaces returns all workspaces
|
||||
func (r *SQLiteRepository) ListWorkspaces(ctx context.Context) ([]*models.Workspace, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT id, name, description, owner_id, created_at, updated_at FROM workspaces ORDER BY created_at DESC
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []*models.Workspace
|
||||
for rows.Next() {
|
||||
workspace := &models.Workspace{}
|
||||
if err := rows.Scan(&workspace.ID, &workspace.Name, &workspace.Description, &workspace.OwnerID, &workspace.CreatedAt, &workspace.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, workspace)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
// Task operations
|
||||
|
||||
// CreateTask creates a new task
|
||||
@@ -138,12 +375,28 @@ func (r *SQLiteRepository) CreateTask(ctx context.Context, task *models.Task) er
|
||||
metadata = []byte("{}")
|
||||
}
|
||||
|
||||
_, err = r.db.ExecContext(ctx, `
|
||||
INSERT INTO tasks (id, board_id, column_id, title, description, state, priority, agent_type, repository_url, branch, assigned_to, position, metadata, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, task.ID, task.BoardID, task.ColumnID, task.Title, task.Description, task.State, task.Priority, task.AgentType, task.RepositoryURL, task.Branch, task.AssignedTo, task.Position, string(metadata), task.CreatedAt, task.UpdatedAt)
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO tasks (id, workspace_id, board_id, column_id, title, description, state, priority, agent_type, repository_url, branch, assigned_to, position, metadata, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, task.ID, task.WorkspaceID, task.BoardID, task.ColumnID, task.Title, task.Description, task.State, task.Priority, task.AgentType, task.RepositoryURL, task.Branch, task.AssignedTo, task.Position, string(metadata), task.CreatedAt, task.UpdatedAt)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
if task.BoardID != "" {
|
||||
if err := r.addTaskToBoardTx(ctx, tx, task.ID, task.BoardID, task.ColumnID, task.Position); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// GetTask retrieves a task by ID
|
||||
@@ -152,9 +405,9 @@ func (r *SQLiteRepository) GetTask(ctx context.Context, id string) (*models.Task
|
||||
var metadata string
|
||||
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT id, board_id, column_id, title, description, state, priority, agent_type, repository_url, branch, assigned_to, position, metadata, created_at, updated_at
|
||||
SELECT id, workspace_id, board_id, column_id, title, description, state, priority, agent_type, repository_url, branch, assigned_to, position, metadata, created_at, updated_at
|
||||
FROM tasks WHERE id = ?
|
||||
`, id).Scan(&task.ID, &task.BoardID, &task.ColumnID, &task.Title, &task.Description, &task.State, &task.Priority, &task.AgentType, &task.RepositoryURL, &task.Branch, &task.AssignedTo, &task.Position, &metadata, &task.CreatedAt, &task.UpdatedAt)
|
||||
`, id).Scan(&task.ID, &task.WorkspaceID, &task.BoardID, &task.ColumnID, &task.Title, &task.Description, &task.State, &task.Priority, &task.AgentType, &task.RepositoryURL, &task.Branch, &task.AssignedTo, &task.Position, &metadata, &task.CreatedAt, &task.UpdatedAt)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("task not found: %s", id)
|
||||
@@ -177,9 +430,9 @@ func (r *SQLiteRepository) UpdateTask(ctx context.Context, task *models.Task) er
|
||||
}
|
||||
|
||||
result, err := r.db.ExecContext(ctx, `
|
||||
UPDATE tasks SET board_id = ?, column_id = ?, title = ?, description = ?, state = ?, priority = ?, agent_type = ?, repository_url = ?, branch = ?, assigned_to = ?, position = ?, metadata = ?, updated_at = ?
|
||||
UPDATE tasks SET workspace_id = ?, board_id = ?, column_id = ?, title = ?, description = ?, state = ?, priority = ?, agent_type = ?, repository_url = ?, branch = ?, assigned_to = ?, position = ?, metadata = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
`, task.BoardID, task.ColumnID, task.Title, task.Description, task.State, task.Priority, task.AgentType, task.RepositoryURL, task.Branch, task.AssignedTo, task.Position, string(metadata), task.UpdatedAt, task.ID)
|
||||
`, task.WorkspaceID, task.BoardID, task.ColumnID, task.Title, task.Description, task.State, task.Priority, task.AgentType, task.RepositoryURL, task.Branch, task.AssignedTo, task.Position, string(metadata), task.UpdatedAt, task.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -188,6 +441,12 @@ func (r *SQLiteRepository) UpdateTask(ctx context.Context, task *models.Task) er
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("task not found: %s", task.ID)
|
||||
}
|
||||
if task.BoardID != "" {
|
||||
if err := r.AddTaskToBoard(ctx, task.ID, task.BoardID, task.ColumnID, task.Position); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -208,29 +467,36 @@ func (r *SQLiteRepository) DeleteTask(ctx context.Context, id string) error {
|
||||
// ListTasks returns all tasks for a board
|
||||
func (r *SQLiteRepository) ListTasks(ctx context.Context, boardID string) ([]*models.Task, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT id, board_id, column_id, title, description, state, priority, agent_type, repository_url, branch, assigned_to, position, metadata, created_at, updated_at
|
||||
FROM tasks WHERE board_id = ? ORDER BY position
|
||||
SELECT t.id, t.workspace_id, t.title, t.description, t.state, t.priority, t.agent_type, t.repository_url, t.branch, t.assigned_to, t.metadata, t.created_at, t.updated_at,
|
||||
tc.board_id, tc.column_id, tc.position
|
||||
FROM tasks t
|
||||
INNER JOIN task_columns tc ON tc.task_id = t.id
|
||||
WHERE tc.board_id = ?
|
||||
ORDER BY tc.position
|
||||
`, boardID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanTasks(rows)
|
||||
return r.scanTasksWithPlacement(rows)
|
||||
}
|
||||
|
||||
// ListTasksByColumn returns all tasks in a column
|
||||
func (r *SQLiteRepository) ListTasksByColumn(ctx context.Context, columnID string) ([]*models.Task, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT id, board_id, column_id, title, description, state, priority, agent_type, repository_url, branch, assigned_to, position, metadata, created_at, updated_at
|
||||
FROM tasks WHERE column_id = ? ORDER BY position
|
||||
SELECT t.id, t.workspace_id, t.title, t.description, t.state, t.priority, t.agent_type, t.repository_url, t.branch, t.assigned_to, t.metadata, t.created_at, t.updated_at,
|
||||
tc.board_id, tc.column_id, tc.position
|
||||
FROM tasks t
|
||||
INNER JOIN task_columns tc ON tc.task_id = t.id
|
||||
WHERE tc.column_id = ? ORDER BY tc.position
|
||||
`, columnID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanTasks(rows)
|
||||
return r.scanTasksWithPlacement(rows)
|
||||
}
|
||||
|
||||
// scanTasks scans multiple task rows
|
||||
@@ -239,7 +505,39 @@ func (r *SQLiteRepository) scanTasks(rows *sql.Rows) ([]*models.Task, error) {
|
||||
for rows.Next() {
|
||||
task := &models.Task{}
|
||||
var metadata string
|
||||
err := rows.Scan(&task.ID, &task.BoardID, &task.ColumnID, &task.Title, &task.Description, &task.State, &task.Priority, &task.AgentType, &task.RepositoryURL, &task.Branch, &task.AssignedTo, &task.Position, &metadata, &task.CreatedAt, &task.UpdatedAt)
|
||||
err := rows.Scan(&task.ID, &task.WorkspaceID, &task.BoardID, &task.ColumnID, &task.Title, &task.Description, &task.State, &task.Priority, &task.AgentType, &task.RepositoryURL, &task.Branch, &task.AssignedTo, &task.Position, &metadata, &task.CreatedAt, &task.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = json.Unmarshal([]byte(metadata), &task.Metadata)
|
||||
result = append(result, task)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (r *SQLiteRepository) scanTasksWithPlacement(rows *sql.Rows) ([]*models.Task, error) {
|
||||
var result []*models.Task
|
||||
for rows.Next() {
|
||||
task := &models.Task{}
|
||||
var metadata string
|
||||
err := rows.Scan(
|
||||
&task.ID,
|
||||
&task.WorkspaceID,
|
||||
&task.Title,
|
||||
&task.Description,
|
||||
&task.State,
|
||||
&task.Priority,
|
||||
&task.AgentType,
|
||||
&task.RepositoryURL,
|
||||
&task.Branch,
|
||||
&task.AssignedTo,
|
||||
&metadata,
|
||||
&task.CreatedAt,
|
||||
&task.UpdatedAt,
|
||||
&task.BoardID,
|
||||
&task.ColumnID,
|
||||
&task.Position,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -263,6 +561,46 @@ func (r *SQLiteRepository) UpdateTaskState(ctx context.Context, id string, state
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddTaskToBoard adds a task to a board with placement
|
||||
func (r *SQLiteRepository) AddTaskToBoard(ctx context.Context, taskID, boardID, columnID string, position int) error {
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
INSERT OR IGNORE INTO task_boards (task_id, board_id) VALUES (?, ?)
|
||||
`, taskID, boardID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = r.db.ExecContext(ctx, `
|
||||
INSERT INTO task_columns (task_id, board_id, column_id, position)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(task_id, board_id) DO UPDATE SET column_id = excluded.column_id, position = excluded.position
|
||||
`, taskID, boardID, columnID, position)
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveTaskFromBoard removes a task from a board
|
||||
func (r *SQLiteRepository) RemoveTaskFromBoard(ctx context.Context, taskID, boardID string) error {
|
||||
if _, err := r.db.ExecContext(ctx, `DELETE FROM task_columns WHERE task_id = ? AND board_id = ?`, taskID, boardID); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := r.db.ExecContext(ctx, `DELETE FROM task_boards WHERE task_id = ? AND board_id = ?`, taskID, boardID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *SQLiteRepository) addTaskToBoardTx(ctx context.Context, tx *sql.Tx, taskID, boardID, columnID string, position int) error {
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT OR IGNORE INTO task_boards (task_id, board_id) VALUES (?, ?)
|
||||
`, taskID, boardID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO task_columns (task_id, board_id, column_id, position)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(task_id, board_id) DO UPDATE SET column_id = excluded.column_id, position = excluded.position
|
||||
`, taskID, boardID, columnID, position)
|
||||
return err
|
||||
}
|
||||
|
||||
// Board operations
|
||||
|
||||
// CreateBoard creates a new board
|
||||
@@ -275,9 +613,9 @@ func (r *SQLiteRepository) CreateBoard(ctx context.Context, board *models.Board)
|
||||
board.UpdatedAt = now
|
||||
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO boards (id, name, description, owner_id, created_at, updated_at)
|
||||
INSERT INTO boards (id, workspace_id, name, description, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`, board.ID, board.Name, board.Description, board.OwnerID, board.CreatedAt, board.UpdatedAt)
|
||||
`, board.ID, board.WorkspaceID, board.Name, board.Description, board.CreatedAt, board.UpdatedAt)
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -287,9 +625,9 @@ func (r *SQLiteRepository) GetBoard(ctx context.Context, id string) (*models.Boa
|
||||
board := &models.Board{}
|
||||
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT id, name, description, owner_id, created_at, updated_at
|
||||
SELECT id, workspace_id, name, description, created_at, updated_at
|
||||
FROM boards WHERE id = ?
|
||||
`, id).Scan(&board.ID, &board.Name, &board.Description, &board.OwnerID, &board.CreatedAt, &board.UpdatedAt)
|
||||
`, id).Scan(&board.ID, &board.WorkspaceID, &board.Name, &board.Description, &board.CreatedAt, &board.UpdatedAt)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("board not found: %s", id)
|
||||
@@ -330,10 +668,18 @@ func (r *SQLiteRepository) DeleteBoard(ctx context.Context, id string) error {
|
||||
}
|
||||
|
||||
// ListBoards returns all boards
|
||||
func (r *SQLiteRepository) ListBoards(ctx context.Context) ([]*models.Board, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT id, name, description, owner_id, created_at, updated_at FROM boards ORDER BY created_at DESC
|
||||
`)
|
||||
func (r *SQLiteRepository) ListBoards(ctx context.Context, workspaceID string) ([]*models.Board, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, name, description, created_at, updated_at FROM boards
|
||||
`
|
||||
var args []interface{}
|
||||
if workspaceID != "" {
|
||||
query += " WHERE workspace_id = ?"
|
||||
args = append(args, workspaceID)
|
||||
}
|
||||
query += " ORDER BY created_at DESC"
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -342,7 +688,7 @@ func (r *SQLiteRepository) ListBoards(ctx context.Context) ([]*models.Board, err
|
||||
var result []*models.Board
|
||||
for rows.Next() {
|
||||
board := &models.Board{}
|
||||
err := rows.Scan(&board.ID, &board.Name, &board.Description, &board.OwnerID, &board.CreatedAt, &board.UpdatedAt)
|
||||
err := rows.Scan(&board.ID, &board.WorkspaceID, &board.Name, &board.Description, &board.CreatedAt, &board.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -55,8 +55,13 @@ func TestSQLiteRepository_BoardCRUD(t *testing.T) {
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
workspace := &models.Workspace{ID: "ws-1", Name: "Workspace"}
|
||||
if err := repo.CreateWorkspace(ctx, workspace); err != nil {
|
||||
t.Fatalf("failed to create workspace: %v", err)
|
||||
}
|
||||
|
||||
// Create
|
||||
board := &models.Board{Name: "Test Board", Description: "A test board", OwnerID: "owner-123"}
|
||||
board := &models.Board{WorkspaceID: workspace.ID, Name: "Test Board", Description: "A test board"}
|
||||
if err := repo.CreateBoard(ctx, board); err != nil {
|
||||
t.Fatalf("failed to create board: %v", err)
|
||||
}
|
||||
@@ -122,16 +127,19 @@ func TestSQLiteRepository_ListBoards(t *testing.T) {
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-1", Name: "Board 1"})
|
||||
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-2", Name: "Board 2"})
|
||||
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-3", Name: "Board 3"})
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace 1"})
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-2", Name: "Workspace 2"})
|
||||
|
||||
boards, err := repo.ListBoards(ctx)
|
||||
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-1", WorkspaceID: "ws-1", Name: "Board 1"})
|
||||
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-2", WorkspaceID: "ws-1", Name: "Board 2"})
|
||||
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-3", WorkspaceID: "ws-2", Name: "Board 3"})
|
||||
|
||||
boards, err := repo.ListBoards(ctx, "ws-1")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to list boards: %v", err)
|
||||
}
|
||||
if len(boards) != 3 {
|
||||
t.Errorf("expected 3 boards, got %d", len(boards))
|
||||
if len(boards) != 2 {
|
||||
t.Errorf("expected 2 boards, got %d", len(boards))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,8 +150,9 @@ func TestSQLiteRepository_ColumnCRUD(t *testing.T) {
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
// First create a board for foreign key
|
||||
board := &models.Board{ID: "board-123", Name: "Test Board"}
|
||||
// First create a workspace and board for foreign key
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
|
||||
board := &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Test Board"}
|
||||
_ = repo.CreateBoard(ctx, board)
|
||||
|
||||
// Create column
|
||||
@@ -211,7 +220,8 @@ func TestSQLiteRepository_ListColumns(t *testing.T) {
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
board := &models.Board{ID: "board-123", Name: "Test Board"}
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
|
||||
board := &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Test Board"}
|
||||
_ = repo.CreateBoard(ctx, board)
|
||||
|
||||
_ = repo.CreateColumn(ctx, &models.Column{ID: "col-1", BoardID: "board-123", Name: "To Do"})
|
||||
@@ -233,14 +243,16 @@ func TestSQLiteRepository_TaskCRUD(t *testing.T) {
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
// Create board and column for foreign keys
|
||||
board := &models.Board{ID: "board-123", Name: "Test Board"}
|
||||
// Create workspace, board, and column for foreign keys
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
|
||||
board := &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Test Board"}
|
||||
_ = repo.CreateBoard(ctx, board)
|
||||
column := &models.Column{ID: "col-123", BoardID: "board-123", Name: "To Do"}
|
||||
_ = repo.CreateColumn(ctx, column)
|
||||
|
||||
// Create task
|
||||
task := &models.Task{
|
||||
WorkspaceID: "ws-1",
|
||||
BoardID: "board-123",
|
||||
ColumnID: "col-123",
|
||||
Title: "Test Task",
|
||||
@@ -314,12 +326,13 @@ func TestSQLiteRepository_UpdateTaskState(t *testing.T) {
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
// Create board, column, and task
|
||||
board := &models.Board{ID: "board-123", Name: "Test Board"}
|
||||
// Create workspace, board, column, and task
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
|
||||
board := &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Test Board"}
|
||||
_ = repo.CreateBoard(ctx, board)
|
||||
column := &models.Column{ID: "col-123", BoardID: "board-123", Name: "To Do"}
|
||||
_ = repo.CreateColumn(ctx, column)
|
||||
task := &models.Task{ID: "task-123", BoardID: "board-123", ColumnID: "col-123", Title: "Test", State: v1.TaskStateTODO}
|
||||
task := &models.Task{ID: "task-123", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-123", Title: "Test", State: v1.TaskStateTODO}
|
||||
_ = repo.CreateTask(ctx, task)
|
||||
|
||||
err := repo.UpdateTaskState(ctx, "task-123", v1.TaskStateInProgress)
|
||||
@@ -349,14 +362,15 @@ func TestSQLiteRepository_ListTasks(t *testing.T) {
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
// Create board and column
|
||||
board := &models.Board{ID: "board-123", Name: "Test Board"}
|
||||
// Create workspace, board, and column
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
|
||||
board := &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Test Board"}
|
||||
_ = repo.CreateBoard(ctx, board)
|
||||
column := &models.Column{ID: "col-123", BoardID: "board-123", Name: "To Do"}
|
||||
_ = repo.CreateColumn(ctx, column)
|
||||
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-1", BoardID: "board-123", ColumnID: "col-123", Title: "Task 1"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-2", BoardID: "board-123", ColumnID: "col-123", Title: "Task 2"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-1", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-123", Title: "Task 1"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-2", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-123", Title: "Task 2"})
|
||||
|
||||
tasks, err := repo.ListTasks(ctx, "board-123")
|
||||
if err != nil {
|
||||
@@ -372,17 +386,18 @@ func TestSQLiteRepository_ListTasksByColumn(t *testing.T) {
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
// Create board and columns
|
||||
board := &models.Board{ID: "board-123", Name: "Test Board"}
|
||||
// Create workspace, board, and columns
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
|
||||
board := &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Test Board"}
|
||||
_ = repo.CreateBoard(ctx, board)
|
||||
col1 := &models.Column{ID: "col-1", BoardID: "board-123", Name: "To Do"}
|
||||
col2 := &models.Column{ID: "col-2", BoardID: "board-123", Name: "Done"}
|
||||
_ = repo.CreateColumn(ctx, col1)
|
||||
_ = repo.CreateColumn(ctx, col2)
|
||||
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-1", BoardID: "board-123", ColumnID: "col-1", Title: "Task 1"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-2", BoardID: "board-123", ColumnID: "col-1", Title: "Task 2"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-3", BoardID: "board-123", ColumnID: "col-2", Title: "Task 3"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-1", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-1", Title: "Task 1"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-2", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-1", Title: "Task 2"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-3", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-2", Title: "Task 3"})
|
||||
|
||||
tasks, err := repo.ListTasksByColumn(ctx, "col-1")
|
||||
if err != nil {
|
||||
@@ -404,7 +419,8 @@ func TestSQLiteRepository_Persistence(t *testing.T) {
|
||||
t.Fatalf("failed to create first repository: %v", err)
|
||||
}
|
||||
|
||||
board := &models.Board{ID: "persist-board", Name: "Persistent Board"}
|
||||
_ = repo1.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
|
||||
board := &models.Board{ID: "persist-board", WorkspaceID: "ws-1", Name: "Persistent Board"}
|
||||
_ = repo1.CreateBoard(ctx, board)
|
||||
repo1.Close()
|
||||
|
||||
@@ -423,4 +439,3 @@ func TestSQLiteRepository_Persistence(t *testing.T) {
|
||||
t.Errorf("expected name 'Persistent Board', got %s", retrieved.Name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ func NewService(repo repository.Repository, eventBus bus.EventBus, log *logger.L
|
||||
|
||||
// CreateTaskRequest contains the data for creating a new task
|
||||
type CreateTaskRequest struct {
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
BoardID string `json:"board_id"`
|
||||
ColumnID string `json:"column_id"`
|
||||
Title string `json:"title"`
|
||||
@@ -61,13 +62,26 @@ type UpdateTaskRequest struct {
|
||||
|
||||
// CreateBoardRequest contains the data for creating a new board
|
||||
type CreateBoardRequest struct {
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// UpdateBoardRequest contains the data for updating a board
|
||||
type UpdateBoardRequest struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// CreateWorkspaceRequest contains the data for creating a new workspace
|
||||
type CreateWorkspaceRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
}
|
||||
|
||||
// UpdateBoardRequest contains the data for updating a board
|
||||
type UpdateBoardRequest struct {
|
||||
// UpdateWorkspaceRequest contains the data for updating a workspace
|
||||
type UpdateWorkspaceRequest struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
@@ -86,6 +100,7 @@ type CreateColumnRequest struct {
|
||||
func (s *Service) CreateTask(ctx context.Context, req *CreateTaskRequest) (*models.Task, error) {
|
||||
task := &models.Task{
|
||||
ID: uuid.New().String(),
|
||||
WorkspaceID: req.WorkspaceID,
|
||||
BoardID: req.BoardID,
|
||||
ColumnID: req.ColumnID,
|
||||
Title: req.Title,
|
||||
@@ -235,7 +250,7 @@ func (s *Service) UpdateTaskMetadata(ctx context.Context, id string, metadata ma
|
||||
}
|
||||
|
||||
// MoveTask moves a task to a different column and position
|
||||
func (s *Service) MoveTask(ctx context.Context, id string, columnID string, position int) (*models.Task, error) {
|
||||
func (s *Service) MoveTask(ctx context.Context, id string, boardID string, columnID string, position int) (*models.Task, error) {
|
||||
task, err := s.repo.GetTask(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -248,6 +263,7 @@ func (s *Service) MoveTask(ctx context.Context, id string, columnID string, posi
|
||||
}
|
||||
|
||||
oldState := task.State
|
||||
task.BoardID = boardID
|
||||
task.ColumnID = columnID
|
||||
task.Position = position
|
||||
task.State = column.State
|
||||
@@ -267,21 +283,86 @@ func (s *Service) MoveTask(ctx context.Context, id string, columnID string, posi
|
||||
|
||||
s.logger.Info("task moved",
|
||||
zap.String("task_id", id),
|
||||
zap.String("board_id", boardID),
|
||||
zap.String("column_id", columnID),
|
||||
zap.Int("position", position))
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// Workspace operations
|
||||
|
||||
// CreateWorkspace creates a new workspace
|
||||
func (s *Service) CreateWorkspace(ctx context.Context, req *CreateWorkspaceRequest) (*models.Workspace, error) {
|
||||
workspace := &models.Workspace{
|
||||
ID: uuid.New().String(),
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
OwnerID: req.OwnerID,
|
||||
}
|
||||
|
||||
if err := s.repo.CreateWorkspace(ctx, workspace); err != nil {
|
||||
s.logger.Error("failed to create workspace", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("workspace created", zap.String("workspace_id", workspace.ID), zap.String("name", workspace.Name))
|
||||
return workspace, nil
|
||||
}
|
||||
|
||||
// GetWorkspace retrieves a workspace by ID
|
||||
func (s *Service) GetWorkspace(ctx context.Context, id string) (*models.Workspace, error) {
|
||||
return s.repo.GetWorkspace(ctx, id)
|
||||
}
|
||||
|
||||
// UpdateWorkspace updates an existing workspace
|
||||
func (s *Service) UpdateWorkspace(ctx context.Context, id string, req *UpdateWorkspaceRequest) (*models.Workspace, error) {
|
||||
workspace, err := s.repo.GetWorkspace(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
workspace.Name = *req.Name
|
||||
}
|
||||
if req.Description != nil {
|
||||
workspace.Description = *req.Description
|
||||
}
|
||||
workspace.UpdatedAt = time.Now().UTC()
|
||||
|
||||
if err := s.repo.UpdateWorkspace(ctx, workspace); err != nil {
|
||||
s.logger.Error("failed to update workspace", zap.String("workspace_id", id), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("workspace updated", zap.String("workspace_id", workspace.ID))
|
||||
return workspace, nil
|
||||
}
|
||||
|
||||
// DeleteWorkspace deletes a workspace
|
||||
func (s *Service) DeleteWorkspace(ctx context.Context, id string) error {
|
||||
if err := s.repo.DeleteWorkspace(ctx, id); err != nil {
|
||||
s.logger.Error("failed to delete workspace", zap.String("workspace_id", id), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
s.logger.Info("workspace deleted", zap.String("workspace_id", id))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListWorkspaces returns all workspaces
|
||||
func (s *Service) ListWorkspaces(ctx context.Context) ([]*models.Workspace, error) {
|
||||
return s.repo.ListWorkspaces(ctx)
|
||||
}
|
||||
|
||||
// Board operations
|
||||
|
||||
// CreateBoard creates a new board
|
||||
func (s *Service) CreateBoard(ctx context.Context, req *CreateBoardRequest) (*models.Board, error) {
|
||||
board := &models.Board{
|
||||
ID: uuid.New().String(),
|
||||
WorkspaceID: req.WorkspaceID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
OwnerID: req.OwnerID,
|
||||
}
|
||||
|
||||
if err := s.repo.CreateBoard(ctx, board); err != nil {
|
||||
@@ -333,9 +414,9 @@ func (s *Service) DeleteBoard(ctx context.Context, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListBoards returns all boards
|
||||
func (s *Service) ListBoards(ctx context.Context) ([]*models.Board, error) {
|
||||
return s.repo.ListBoards(ctx)
|
||||
// ListBoards returns all boards for a workspace (or all if empty)
|
||||
func (s *Service) ListBoards(ctx context.Context, workspaceID string) ([]*models.Board, error) {
|
||||
return s.repo.ListBoards(ctx, workspaceID)
|
||||
}
|
||||
|
||||
// Column operations
|
||||
@@ -415,4 +496,3 @@ func (s *Service) publishTaskEvent(ctx context.Context, eventType string, task *
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
// MockEventBus implements bus.EventBus for testing
|
||||
type MockEventBus struct {
|
||||
mu sync.Mutex
|
||||
mu sync.Mutex
|
||||
publishedEvents []*bus.Event
|
||||
closed bool
|
||||
}
|
||||
@@ -83,12 +83,14 @@ func TestService_CreateTask(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create board and column first
|
||||
board := &models.Board{ID: "board-123", Name: "Test Board"}
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
|
||||
board := &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Test Board"}
|
||||
_ = repo.CreateBoard(ctx, board)
|
||||
column := &models.Column{ID: "col-123", BoardID: "board-123", Name: "To Do", State: v1.TaskStateTODO}
|
||||
_ = repo.CreateColumn(ctx, column)
|
||||
|
||||
req := &CreateTaskRequest{
|
||||
WorkspaceID: "ws-1",
|
||||
BoardID: "board-123",
|
||||
ColumnID: "col-123",
|
||||
Title: "Test Task",
|
||||
@@ -126,7 +128,7 @@ func TestService_GetTask(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a task directly in repo
|
||||
task := &models.Task{ID: "task-123", BoardID: "board-123", ColumnID: "col-123", Title: "Test Task"}
|
||||
task := &models.Task{ID: "task-123", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-123", Title: "Test Task"}
|
||||
_ = repo.CreateTask(ctx, task)
|
||||
|
||||
retrieved, err := svc.GetTask(ctx, "task-123")
|
||||
@@ -152,7 +154,7 @@ func TestService_UpdateTask(t *testing.T) {
|
||||
svc, eventBus, repo := createTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
task := &models.Task{ID: "task-123", BoardID: "board-123", ColumnID: "col-123", Title: "Original"}
|
||||
task := &models.Task{ID: "task-123", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-123", Title: "Original"}
|
||||
_ = repo.CreateTask(ctx, task)
|
||||
eventBus.ClearEvents()
|
||||
|
||||
@@ -181,7 +183,7 @@ func TestService_DeleteTask(t *testing.T) {
|
||||
svc, eventBus, repo := createTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
task := &models.Task{ID: "task-123", BoardID: "board-123", ColumnID: "col-123", Title: "Test"}
|
||||
task := &models.Task{ID: "task-123", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-123", Title: "Test"}
|
||||
_ = repo.CreateTask(ctx, task)
|
||||
eventBus.ClearEvents()
|
||||
|
||||
@@ -207,8 +209,8 @@ func TestService_ListTasks(t *testing.T) {
|
||||
svc, _, repo := createTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-1", BoardID: "board-123", ColumnID: "col-123", Title: "Task 1"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-2", BoardID: "board-123", ColumnID: "col-123", Title: "Task 2"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-1", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-123", Title: "Task 1"})
|
||||
_ = repo.CreateTask(ctx, &models.Task{ID: "task-2", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-123", Title: "Task 2"})
|
||||
|
||||
tasks, err := svc.ListTasks(ctx, "board-123")
|
||||
if err != nil {
|
||||
@@ -223,7 +225,8 @@ func TestService_UpdateTaskState(t *testing.T) {
|
||||
svc, eventBus, repo := createTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
task := &models.Task{ID: "task-123", BoardID: "board-123", ColumnID: "col-123", Title: "Test", State: v1.TaskStateTODO}
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
|
||||
task := &models.Task{ID: "task-123", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-123", Title: "Test", State: v1.TaskStateTODO}
|
||||
_ = repo.CreateTask(ctx, task)
|
||||
eventBus.ClearEvents()
|
||||
|
||||
@@ -249,15 +252,18 @@ func TestService_MoveTask(t *testing.T) {
|
||||
svc, eventBus, repo := createTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
|
||||
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Board"})
|
||||
|
||||
// Create column with different state
|
||||
column := &models.Column{ID: "col-done", BoardID: "board-123", Name: "Done", State: v1.TaskStateCompleted}
|
||||
_ = repo.CreateColumn(ctx, column)
|
||||
|
||||
task := &models.Task{ID: "task-123", BoardID: "board-123", ColumnID: "col-123", Title: "Test", State: v1.TaskStateTODO}
|
||||
task := &models.Task{ID: "task-123", WorkspaceID: "ws-1", BoardID: "board-123", ColumnID: "col-123", Title: "Test", State: v1.TaskStateTODO}
|
||||
_ = repo.CreateTask(ctx, task)
|
||||
eventBus.ClearEvents()
|
||||
|
||||
moved, err := svc.MoveTask(ctx, "task-123", "col-done", 0)
|
||||
moved, err := svc.MoveTask(ctx, "task-123", "board-123", "col-done", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to move task: %v", err)
|
||||
}
|
||||
@@ -272,13 +278,14 @@ func TestService_MoveTask(t *testing.T) {
|
||||
// Board tests
|
||||
|
||||
func TestService_CreateBoard(t *testing.T) {
|
||||
svc, _, _ := createTestService(t)
|
||||
svc, _, repo := createTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
|
||||
req := &CreateBoardRequest{
|
||||
WorkspaceID: "ws-1",
|
||||
Name: "Test Board",
|
||||
Description: "A test board",
|
||||
OwnerID: "owner-123",
|
||||
}
|
||||
|
||||
board, err := svc.CreateBoard(ctx, req)
|
||||
@@ -297,7 +304,8 @@ func TestService_GetBoard(t *testing.T) {
|
||||
svc, _, repo := createTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
board := &models.Board{ID: "board-123", Name: "Test Board"}
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
|
||||
board := &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Test Board"}
|
||||
_ = repo.CreateBoard(ctx, board)
|
||||
|
||||
retrieved, err := svc.GetBoard(ctx, "board-123")
|
||||
@@ -313,7 +321,8 @@ func TestService_UpdateBoard(t *testing.T) {
|
||||
svc, _, repo := createTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
board := &models.Board{ID: "board-123", Name: "Original"}
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
|
||||
board := &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Original"}
|
||||
_ = repo.CreateBoard(ctx, board)
|
||||
|
||||
newName := "Updated"
|
||||
@@ -327,13 +336,12 @@ func TestService_UpdateBoard(t *testing.T) {
|
||||
t.Errorf("expected name 'Updated', got %s", updated.Name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func TestService_DeleteBoard(t *testing.T) {
|
||||
svc, _, repo := createTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
board := &models.Board{ID: "board-123", Name: "Test Board"}
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
|
||||
board := &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Test Board"}
|
||||
_ = repo.CreateBoard(ctx, board)
|
||||
|
||||
err := svc.DeleteBoard(ctx, "board-123")
|
||||
@@ -351,10 +359,11 @@ func TestService_ListBoards(t *testing.T) {
|
||||
svc, _, repo := createTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-1", Name: "Board 1"})
|
||||
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-2", Name: "Board 2"})
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
|
||||
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-1", WorkspaceID: "ws-1", Name: "Board 1"})
|
||||
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-2", WorkspaceID: "ws-1", Name: "Board 2"})
|
||||
|
||||
boards, err := svc.ListBoards(ctx)
|
||||
boards, err := svc.ListBoards(ctx, "ws-1")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to list boards: %v", err)
|
||||
}
|
||||
@@ -369,7 +378,8 @@ func TestService_CreateColumn(t *testing.T) {
|
||||
svc, _, repo := createTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
board := &models.Board{ID: "board-123", Name: "Test Board"}
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
|
||||
board := &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Test Board"}
|
||||
_ = repo.CreateBoard(ctx, board)
|
||||
|
||||
req := &CreateColumnRequest{
|
||||
@@ -395,6 +405,8 @@ func TestService_GetColumn(t *testing.T) {
|
||||
svc, _, repo := createTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
|
||||
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Board"})
|
||||
column := &models.Column{ID: "col-123", BoardID: "board-123", Name: "To Do"}
|
||||
_ = repo.CreateColumn(ctx, column)
|
||||
|
||||
@@ -411,6 +423,8 @@ func TestService_ListColumns(t *testing.T) {
|
||||
svc, _, repo := createTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
|
||||
_ = repo.CreateBoard(ctx, &models.Board{ID: "board-123", WorkspaceID: "ws-1", Name: "Board"})
|
||||
_ = repo.CreateColumn(ctx, &models.Column{ID: "col-1", BoardID: "board-123", Name: "To Do"})
|
||||
_ = repo.CreateColumn(ctx, &models.Column{ID: "col-2", BoardID: "board-123", Name: "Done"})
|
||||
|
||||
@@ -422,4 +436,3 @@ func TestService_ListColumns(t *testing.T) {
|
||||
t.Errorf("expected 2 columns, got %d", len(columns))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ type AgentStatus string
|
||||
const (
|
||||
AgentStatusPending AgentStatus = "PENDING"
|
||||
AgentStatusStarting AgentStatus = "STARTING"
|
||||
AgentStatusRunning AgentStatus = "RUNNING" // Agent is processing a prompt
|
||||
AgentStatusReady AgentStatus = "READY" // Agent finished prompt, waiting for next
|
||||
AgentStatusRunning AgentStatus = "RUNNING" // Agent is processing a prompt
|
||||
AgentStatusReady AgentStatus = "READY" // Agent finished prompt, waiting for next
|
||||
AgentStatusCompleted AgentStatus = "COMPLETED"
|
||||
AgentStatusFailed AgentStatus = "FAILED"
|
||||
AgentStatusStopped AgentStatus = "STOPPED"
|
||||
@@ -64,4 +64,3 @@ type AgentLog struct {
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import "time"
|
||||
// Board represents a Kanban board
|
||||
type Board struct {
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -23,4 +23,3 @@ type UpdateBoardRequest struct {
|
||||
Name *string `json:"name,omitempty" binding:"omitempty,max=255"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
// Task represents a Kanban task
|
||||
type Task struct {
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
BoardID string `json:"board_id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
@@ -72,4 +73,3 @@ type TaskEvent struct {
|
||||
CreatedBy *string `json:"created_by,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
|
||||
@@ -10,4 +10,3 @@ type User struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package v1
|
||||
|
||||
import "time"
|
||||
|
||||
// Workspace represents a workspace
|
||||
type Workspace struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// CreateWorkspaceRequest for creating a new workspace
|
||||
type CreateWorkspaceRequest struct {
|
||||
Name string `json:"name" binding:"required,max=255"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateWorkspaceRequest for updating an existing workspace
|
||||
type UpdateWorkspaceRequest struct {
|
||||
Name *string `json:"name,omitempty" binding:"omitempty,max=255"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
@@ -12,6 +12,13 @@ const (
|
||||
ActionBoardUpdate = "board.update"
|
||||
ActionBoardDelete = "board.delete"
|
||||
|
||||
// Workspace actions
|
||||
ActionWorkspaceList = "workspace.list"
|
||||
ActionWorkspaceCreate = "workspace.create"
|
||||
ActionWorkspaceGet = "workspace.get"
|
||||
ActionWorkspaceUpdate = "workspace.update"
|
||||
ActionWorkspaceDelete = "workspace.delete"
|
||||
|
||||
// Column actions
|
||||
ActionColumnList = "column.list"
|
||||
ActionColumnCreate = "column.create"
|
||||
@@ -54,14 +61,14 @@ const (
|
||||
ActionTaskUnsubscribe = "task.unsubscribe"
|
||||
|
||||
// Notification actions (server -> client)
|
||||
ActionACPProgress = "acp.progress"
|
||||
ActionACPLog = "acp.log"
|
||||
ActionACPResult = "acp.result"
|
||||
ActionACPError = "acp.error"
|
||||
ActionACPStatus = "acp.status"
|
||||
ActionACPHeartbeat = "acp.heartbeat"
|
||||
ActionTaskUpdated = "task.updated"
|
||||
ActionAgentUpdated = "agent.updated"
|
||||
ActionACPProgress = "acp.progress"
|
||||
ActionACPLog = "acp.log"
|
||||
ActionACPResult = "acp.result"
|
||||
ActionACPError = "acp.error"
|
||||
ActionACPStatus = "acp.status"
|
||||
ActionACPHeartbeat = "acp.heartbeat"
|
||||
ActionTaskUpdated = "task.updated"
|
||||
ActionAgentUpdated = "agent.updated"
|
||||
)
|
||||
|
||||
// Error codes
|
||||
@@ -74,4 +81,3 @@ const (
|
||||
ErrorCodeValidation = "VALIDATION_ERROR"
|
||||
ErrorCodeUnknownAction = "UNKNOWN_ACTION"
|
||||
)
|
||||
|
||||
|
||||
@@ -53,4 +53,3 @@ func (d *Dispatcher) HasHandler(action string) bool {
|
||||
_, ok := d.handlers[action]
|
||||
return ok
|
||||
}
|
||||
|
||||
|
||||
@@ -128,4 +128,3 @@ func (m *Message) ParsePayload(v interface{}) error {
|
||||
}
|
||||
return json.Unmarshal(m.Payload, v)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"dev": "next dev",
|
||||
"build": "NODE_ENV=production next build",
|
||||
"start": "NODE_ENV=production next start",
|
||||
"lint": "eslint"
|
||||
"lint": "eslint",
|
||||
"test": "echo \"No tests yet\" && exit 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.0.0",
|
||||
|
||||
Reference in New Issue
Block a user