Add project tasks management to platform projects
This commit is contained in:
parent
d304951767
commit
9d3622cb57
14 changed files with 506 additions and 3 deletions
|
|
@ -5,7 +5,7 @@
|
||||||
## Responsabilidade e limites
|
## Responsabilidade e limites
|
||||||
|
|
||||||
**Este serviço faz:**
|
**Este serviço faz:**
|
||||||
- Governança de projetos, ambientes, repositórios e vínculos externos.
|
- Governança de projetos, ambientes, repositórios, tarefas e vínculos externos.
|
||||||
- Orquestração de metadados com multi-tenancy por design.
|
- Orquestração de metadados com multi-tenancy por design.
|
||||||
- Auditoria e observabilidade para uso enterprise.
|
- Auditoria e observabilidade para uso enterprise.
|
||||||
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
- **Environment**: pertence a um projeto, tipos controlados.
|
- **Environment**: pertence a um projeto, tipos controlados.
|
||||||
- **Repository**: metadados, provider enum, URL validada, branch obrigatória.
|
- **Repository**: metadados, provider enum, URL validada, branch obrigatória.
|
||||||
- **Project Links**: somente IDs externos, meta JSON sem segredo.
|
- **Project Links**: somente IDs externos, meta JSON sem segredo.
|
||||||
|
- **Tasks**: tarefas com status, prazo e vínculo por projeto.
|
||||||
|
|
||||||
## Exemplo de uso
|
## Exemplo de uso
|
||||||
|
|
||||||
|
|
@ -50,6 +51,15 @@ curl -X POST http://localhost:8080/api/v1/environments \
|
||||||
-d '{"project_id":"<project-id>","type":"production"}'
|
-d '{"project_id":"<project-id>","type":"production"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Criar tarefa:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/v1/tasks \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"project_id":"<project-id>","title":"Kickoff","description":"Reunião inicial","due_date":"2024-09-01T12:00:00Z"}'
|
||||||
|
```
|
||||||
|
|
||||||
## Escalabilidade com segurança
|
## Escalabilidade com segurança
|
||||||
|
|
||||||
- Multi-tenancy explícito em todas as tabelas e queries.
|
- Multi-tenancy explícito em todas as tabelas e queries.
|
||||||
|
|
@ -72,4 +82,3 @@ make lint
|
||||||
- Logs estruturados via `slog`.
|
- Logs estruturados via `slog`.
|
||||||
- Endpoint `/api/v1/health`.
|
- Endpoint `/api/v1/health`.
|
||||||
- Endpoint `/api/v1/metrics` (placeholder para instrumentação enterprise).
|
- Endpoint `/api/v1/metrics` (placeholder para instrumentação enterprise).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,17 @@
|
||||||
- `url`
|
- `url`
|
||||||
- `default_branch`
|
- `default_branch`
|
||||||
|
|
||||||
|
## Task
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- `tenant_id`
|
||||||
|
- `project_id`
|
||||||
|
- `title`
|
||||||
|
- `description`
|
||||||
|
- `status` (open, in_progress, done, archived)
|
||||||
|
- `due_date`
|
||||||
|
|
||||||
## Project Links
|
## Project Links
|
||||||
|
|
||||||
- `infra`, `billing`, `security`
|
- `infra`, `billing`, `security`
|
||||||
- apenas IDs externos e `meta` JSON sem segredo
|
- apenas IDs externos e `meta` JSON sem segredo
|
||||||
|
|
||||||
|
|
|
||||||
162
platform-projects-core/internal/api/handlers/tasks_handler.go
Normal file
162
platform-projects-core/internal/api/handlers/tasks_handler.go
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"platform-projects-core/internal/api/transport"
|
||||||
|
"platform-projects-core/internal/application/tasks"
|
||||||
|
"platform-projects-core/internal/infrastructure/repositories"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TasksHandler struct {
|
||||||
|
logger *slog.Logger
|
||||||
|
usecases struct {
|
||||||
|
create tasks.CreateTaskUseCase
|
||||||
|
update tasks.UpdateTaskUseCase
|
||||||
|
archive tasks.ArchiveTaskUseCase
|
||||||
|
list tasks.ListTasksUseCase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTasksHandler(logger *slog.Logger, pool *pgxpool.Pool) http.Handler {
|
||||||
|
repo := repositories.NewTasksRepository(pool)
|
||||||
|
h := &TasksHandler{logger: logger}
|
||||||
|
h.usecases.create = tasks.CreateTaskUseCase{Repo: repo}
|
||||||
|
h.usecases.update = tasks.UpdateTaskUseCase{Repo: repo}
|
||||||
|
h.usecases.archive = tasks.ArchiveTaskUseCase{Repo: repo}
|
||||||
|
h.usecases.list = tasks.ListTasksUseCase{Repo: repo}
|
||||||
|
|
||||||
|
r := chi.NewRouter()
|
||||||
|
r.Post("/", h.create)
|
||||||
|
r.Get("/", h.list)
|
||||||
|
r.Put("/{taskId}", h.update)
|
||||||
|
r.Delete("/{taskId}", h.archive)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
type createTaskRequest struct {
|
||||||
|
ProjectID string `json:"project_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
DueDate *string `json:"due_date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TasksHandler) create(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tenantID, ok := transport.TenantFromContext(r.Context())
|
||||||
|
if !ok {
|
||||||
|
transport.WriteError(w, http.StatusUnauthorized, "unauthorized", "tenant missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req createTaskRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
transport.WriteError(w, http.StatusBadRequest, "invalid_payload", "invalid JSON payload")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dueDate, err := parseDueDate(req.DueDate)
|
||||||
|
if err != nil {
|
||||||
|
transport.WriteError(w, http.StatusBadRequest, "invalid_payload", "invalid due_date")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task, err := h.usecases.create.Execute(r.Context(), tasks.CreateTaskInput{
|
||||||
|
TenantID: tenantID,
|
||||||
|
ProjectID: req.ProjectID,
|
||||||
|
Title: req.Title,
|
||||||
|
Description: req.Description,
|
||||||
|
DueDate: dueDate,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
writeDomainError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
transport.WriteJSON(w, http.StatusCreated, transport.Envelope{"task": task})
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateTaskRequest struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
DueDate *string `json:"due_date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TasksHandler) update(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tenantID, ok := transport.TenantFromContext(r.Context())
|
||||||
|
if !ok {
|
||||||
|
transport.WriteError(w, http.StatusUnauthorized, "unauthorized", "tenant missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
taskID := chi.URLParam(r, "taskId")
|
||||||
|
var req updateTaskRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
transport.WriteError(w, http.StatusBadRequest, "invalid_payload", "invalid JSON payload")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dueDate, err := parseDueDate(req.DueDate)
|
||||||
|
if err != nil {
|
||||||
|
transport.WriteError(w, http.StatusBadRequest, "invalid_payload", "invalid due_date")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task, err := h.usecases.update.Execute(r.Context(), tasks.UpdateTaskInput{
|
||||||
|
TenantID: tenantID,
|
||||||
|
TaskID: taskID,
|
||||||
|
Title: req.Title,
|
||||||
|
Description: req.Description,
|
||||||
|
Status: req.Status,
|
||||||
|
DueDate: dueDate,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
writeDomainError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
transport.WriteJSON(w, http.StatusOK, transport.Envelope{"task": task})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TasksHandler) archive(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tenantID, ok := transport.TenantFromContext(r.Context())
|
||||||
|
if !ok {
|
||||||
|
transport.WriteError(w, http.StatusUnauthorized, "unauthorized", "tenant missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
taskID := chi.URLParam(r, "taskId")
|
||||||
|
if err := h.usecases.archive.Execute(r.Context(), tasks.ArchiveTaskInput{
|
||||||
|
TenantID: tenantID,
|
||||||
|
TaskID: taskID,
|
||||||
|
}); err != nil {
|
||||||
|
writeDomainError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
transport.WriteJSON(w, http.StatusOK, transport.Envelope{"status": "archived"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TasksHandler) list(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tenantID, ok := transport.TenantFromContext(r.Context())
|
||||||
|
if !ok {
|
||||||
|
transport.WriteError(w, http.StatusUnauthorized, "unauthorized", "tenant missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
projectID := r.URL.Query().Get("project_id")
|
||||||
|
tasksList, err := h.usecases.list.Execute(r.Context(), tasks.ListTasksInput{
|
||||||
|
TenantID: tenantID,
|
||||||
|
ProjectID: projectID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
writeDomainError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
transport.WriteJSON(w, http.StatusOK, transport.Envelope{"tasks": tasksList})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDueDate(value *string) (*time.Time, error) {
|
||||||
|
if value == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
parsed, err := time.Parse(time.RFC3339, *value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &parsed, nil
|
||||||
|
}
|
||||||
|
|
@ -44,6 +44,7 @@ func NewRouter(deps RouterDependencies) http.Handler {
|
||||||
r.Mount("/environments", handlers.NewEnvironmentsHandler(logger, deps.DB))
|
r.Mount("/environments", handlers.NewEnvironmentsHandler(logger, deps.DB))
|
||||||
r.Mount("/repositories", handlers.NewRepositoriesHandler(logger, deps.DB))
|
r.Mount("/repositories", handlers.NewRepositoriesHandler(logger, deps.DB))
|
||||||
r.Mount("/teams", handlers.NewTeamsHandler(logger, deps.DB))
|
r.Mount("/teams", handlers.NewTeamsHandler(logger, deps.DB))
|
||||||
|
r.Mount("/tasks", handlers.NewTasksHandler(logger, deps.DB))
|
||||||
r.Mount("/project-links", handlers.NewProjectLinksHandler(logger, deps.DB))
|
r.Mount("/project-links", handlers.NewProjectLinksHandler(logger, deps.DB))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package tasks
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type ArchiveTaskUseCase struct {
|
||||||
|
Repo TaskRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArchiveTaskInput struct {
|
||||||
|
TenantID string
|
||||||
|
TaskID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc ArchiveTaskUseCase) Execute(ctx context.Context, input ArchiveTaskInput) error {
|
||||||
|
return uc.Repo.Archive(ctx, input.TenantID, input.TaskID)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package tasks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"platform-projects-core/internal/domain/projects"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TaskRepository interface {
|
||||||
|
Create(ctx context.Context, task projects.Task) (projects.Task, error)
|
||||||
|
Update(ctx context.Context, task projects.Task) (projects.Task, error)
|
||||||
|
Archive(ctx context.Context, tenantID, taskID string) error
|
||||||
|
List(ctx context.Context, tenantID, projectID string) ([]projects.Task, error)
|
||||||
|
Find(ctx context.Context, tenantID, taskID string) (projects.Task, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateTaskUseCase struct {
|
||||||
|
Repo TaskRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateTaskInput struct {
|
||||||
|
TenantID string
|
||||||
|
ProjectID string
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
DueDate *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc CreateTaskUseCase) Execute(ctx context.Context, input CreateTaskInput) (projects.Task, error) {
|
||||||
|
task, err := projects.NewTask(input.TenantID, input.ProjectID, input.Title, input.Description, input.DueDate)
|
||||||
|
if err != nil {
|
||||||
|
return projects.Task{}, err
|
||||||
|
}
|
||||||
|
return uc.Repo.Create(ctx, task)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package tasks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"platform-projects-core/internal/domain/projects"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListTasksUseCase struct {
|
||||||
|
Repo TaskRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListTasksInput struct {
|
||||||
|
TenantID string
|
||||||
|
ProjectID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc ListTasksUseCase) Execute(ctx context.Context, input ListTasksInput) ([]projects.Task, error) {
|
||||||
|
return uc.Repo.List(ctx, input.TenantID, input.ProjectID)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package tasks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"platform-projects-core/internal/domain/projects"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdateTaskUseCase struct {
|
||||||
|
Repo TaskRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateTaskInput struct {
|
||||||
|
TenantID string
|
||||||
|
TaskID string
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
Status string
|
||||||
|
DueDate *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc UpdateTaskUseCase) Execute(ctx context.Context, input UpdateTaskInput) (projects.Task, error) {
|
||||||
|
existing, err := uc.Repo.Find(ctx, input.TenantID, input.TaskID)
|
||||||
|
if err != nil {
|
||||||
|
return projects.Task{}, err
|
||||||
|
}
|
||||||
|
status, err := projects.ParseTaskStatus(input.Status)
|
||||||
|
if err != nil {
|
||||||
|
return projects.Task{}, err
|
||||||
|
}
|
||||||
|
updated, err := existing.WithUpdates(input.Title, input.Description, status, input.DueDate)
|
||||||
|
if err != nil {
|
||||||
|
return projects.Task{}, err
|
||||||
|
}
|
||||||
|
return uc.Repo.Update(ctx, updated)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS project_tasks;
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS project_tasks (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
project_id UUID NOT NULL REFERENCES projects(id),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
due_date TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS project_tasks_tenant_idx ON project_tasks (tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS project_tasks_project_idx ON project_tasks (project_id);
|
||||||
26
platform-projects-core/internal/db/queries/tasks.sql
Normal file
26
platform-projects-core/internal/db/queries/tasks.sql
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
-- name: CreateTask :exec
|
||||||
|
INSERT INTO project_tasks (id, tenant_id, project_id, title, description, status, due_date, created_at, updated_at)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9);
|
||||||
|
|
||||||
|
-- name: UpdateTask :exec
|
||||||
|
UPDATE project_tasks
|
||||||
|
SET title = $1,
|
||||||
|
description = $2,
|
||||||
|
status = $3,
|
||||||
|
due_date = $4,
|
||||||
|
updated_at = $5
|
||||||
|
WHERE tenant_id = $6
|
||||||
|
AND id = $7;
|
||||||
|
|
||||||
|
-- name: ArchiveTask :exec
|
||||||
|
UPDATE project_tasks
|
||||||
|
SET status = 'archived',
|
||||||
|
updated_at = $1
|
||||||
|
WHERE tenant_id = $2
|
||||||
|
AND id = $3;
|
||||||
|
|
||||||
|
-- name: ListTasks :many
|
||||||
|
SELECT id, tenant_id, project_id, title, description, status, due_date, created_at, updated_at
|
||||||
|
FROM project_tasks
|
||||||
|
WHERE tenant_id = $1
|
||||||
|
ORDER BY created_at DESC;
|
||||||
51
platform-projects-core/internal/domain/projects/task.go
Normal file
51
platform-projects-core/internal/domain/projects/task.go
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
package projects
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"platform-projects-core/internal/domain/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Task struct {
|
||||||
|
ID string
|
||||||
|
TenantID string
|
||||||
|
ProjectID string
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
Status TaskStatus
|
||||||
|
DueDate *time.Time
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTask(tenantID, projectID, title, description string, dueDate *time.Time) (Task, error) {
|
||||||
|
title = strings.TrimSpace(title)
|
||||||
|
if tenantID == "" || projectID == "" || title == "" {
|
||||||
|
return Task{}, common.ErrInvalidInput
|
||||||
|
}
|
||||||
|
return Task{
|
||||||
|
ID: common.NewID(),
|
||||||
|
TenantID: tenantID,
|
||||||
|
ProjectID: projectID,
|
||||||
|
Title: title,
|
||||||
|
Description: strings.TrimSpace(description),
|
||||||
|
Status: TaskStatusOpen,
|
||||||
|
DueDate: dueDate,
|
||||||
|
CreatedAt: time.Now().UTC(),
|
||||||
|
UpdatedAt: time.Now().UTC(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Task) WithUpdates(title, description string, status TaskStatus, dueDate *time.Time) (Task, error) {
|
||||||
|
title = strings.TrimSpace(title)
|
||||||
|
if title == "" {
|
||||||
|
return Task{}, common.ErrInvalidInput
|
||||||
|
}
|
||||||
|
t.Title = title
|
||||||
|
t.Description = strings.TrimSpace(description)
|
||||||
|
t.Status = status
|
||||||
|
t.DueDate = dueDate
|
||||||
|
t.UpdatedAt = time.Now().UTC()
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package projects
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"platform-projects-core/internal/domain/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TaskStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TaskStatusOpen TaskStatus = "open"
|
||||||
|
TaskStatusInProgress TaskStatus = "in_progress"
|
||||||
|
TaskStatusDone TaskStatus = "done"
|
||||||
|
TaskStatusArchived TaskStatus = "archived"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseTaskStatus(value string) (TaskStatus, error) {
|
||||||
|
switch strings.TrimSpace(value) {
|
||||||
|
case string(TaskStatusOpen):
|
||||||
|
return TaskStatusOpen, nil
|
||||||
|
case string(TaskStatusInProgress):
|
||||||
|
return TaskStatusInProgress, nil
|
||||||
|
case string(TaskStatusDone):
|
||||||
|
return TaskStatusDone, nil
|
||||||
|
case string(TaskStatusArchived):
|
||||||
|
return TaskStatusArchived, nil
|
||||||
|
default:
|
||||||
|
return "", common.ErrInvalidInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"platform-projects-core/internal/domain/common"
|
||||||
|
"platform-projects-core/internal/domain/projects"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TasksRepository struct {
|
||||||
|
pool *pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTasksRepository(pool *pgxpool.Pool) *TasksRepository {
|
||||||
|
return &TasksRepository{pool: pool}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TasksRepository) Create(ctx context.Context, task projects.Task) (projects.Task, error) {
|
||||||
|
query := `INSERT INTO project_tasks (id, tenant_id, project_id, title, description, status, due_date, created_at, updated_at)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`
|
||||||
|
_, err := r.pool.Exec(ctx, query, task.ID, task.TenantID, task.ProjectID, task.Title, task.Description, task.Status, task.DueDate, task.CreatedAt, task.UpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return projects.Task{}, err
|
||||||
|
}
|
||||||
|
return task, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TasksRepository) Update(ctx context.Context, task projects.Task) (projects.Task, error) {
|
||||||
|
query := `UPDATE project_tasks SET title=$1, description=$2, status=$3, due_date=$4, updated_at=$5 WHERE tenant_id=$6 AND id=$7`
|
||||||
|
cmd, err := r.pool.Exec(ctx, query, task.Title, task.Description, task.Status, task.DueDate, time.Now().UTC(), task.TenantID, task.ID)
|
||||||
|
if err != nil {
|
||||||
|
return projects.Task{}, err
|
||||||
|
}
|
||||||
|
if cmd.RowsAffected() == 0 {
|
||||||
|
return projects.Task{}, common.ErrNotFound
|
||||||
|
}
|
||||||
|
return task, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TasksRepository) Archive(ctx context.Context, tenantID, taskID string) error {
|
||||||
|
query := `UPDATE project_tasks SET status='archived', updated_at=$1 WHERE tenant_id=$2 AND id=$3`
|
||||||
|
cmd, err := r.pool.Exec(ctx, query, time.Now().UTC(), tenantID, taskID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cmd.RowsAffected() == 0 {
|
||||||
|
return common.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TasksRepository) List(ctx context.Context, tenantID, projectID string) ([]projects.Task, error) {
|
||||||
|
query := `SELECT id, tenant_id, project_id, title, description, status, due_date, created_at, updated_at FROM project_tasks WHERE tenant_id=$1`
|
||||||
|
args := []any{tenantID}
|
||||||
|
if projectID != "" {
|
||||||
|
query += " AND project_id=$2"
|
||||||
|
args = append(args, projectID)
|
||||||
|
}
|
||||||
|
query += " ORDER BY created_at DESC"
|
||||||
|
rows, err := r.pool.Query(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var results []projects.Task
|
||||||
|
for rows.Next() {
|
||||||
|
var t projects.Task
|
||||||
|
if err := rows.Scan(&t.ID, &t.TenantID, &t.ProjectID, &t.Title, &t.Description, &t.Status, &t.DueDate, &t.CreatedAt, &t.UpdatedAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
results = append(results, t)
|
||||||
|
}
|
||||||
|
return results, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TasksRepository) Find(ctx context.Context, tenantID, taskID string) (projects.Task, error) {
|
||||||
|
query := `SELECT id, tenant_id, project_id, title, description, status, due_date, created_at, updated_at FROM project_tasks WHERE tenant_id=$1 AND id=$2`
|
||||||
|
var t projects.Task
|
||||||
|
if err := r.pool.QueryRow(ctx, query, tenantID, taskID).Scan(&t.ID, &t.TenantID, &t.ProjectID, &t.Title, &t.Description, &t.Status, &t.DueDate, &t.CreatedAt, &t.UpdatedAt); err != nil {
|
||||||
|
if err == pgx.ErrNoRows {
|
||||||
|
return projects.Task{}, common.ErrNotFound
|
||||||
|
}
|
||||||
|
return projects.Task{}, err
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue