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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# для чего нам нужны эти импорты

```
taskdomain "example.com/taskservice/internal/domain/task"
taskusecase "example.com/taskservice/internal/usecase/task"
```

Эти импорты подключают два внутренних пакета и дают им удобные псевдонимы:

* `taskdomain` — пакет домена (entities/ошибки). В нём, например, определено `ErrNotFound` и типы сущности задачи.
* `taskusecase` — пакет бизнес-логики (use-cases). В нём объявлен интерфейс `Usecase`, структуры входных данных (`CreateInput`, `UpdateInput`) и ошибки уровня usecase (`ErrInvalidInput`).

Зачем нужны:

* В `TaskHandler` поле `usecase` имеет тип `taskusecase.Usecase`.
* В методах создаются/обновляются данные через `taskusecase.CreateInput` / `UpdateInput`.
* В `writeUsecaseError` сравниваются ошибки `taskdomain.ErrNotFound` и `taskusecase.ErrInvalidInput`.

Псевдонимы нужны, чтобы:

* избежать конфликта имён (оба пакета могут быть `task`),
* сделать код более явным (чётко видно, откуда тип/ошибка).

---

# Сгенерируй body для post запросов

Example JSON bodies for `POST /api/v1/tasks` (create task). Fields used: `title` (required), `description`, `dueDate` (RFC3339), `completed` (bool), `priority` (int), `tags` (array of strings).

### minimal

```json
{
"title": "Buy groceries"
}
```

### full

```json
{
"title": "Finish report",
"description": "Complete monthly sales report and send to stakeholders.",
"dueDate": "2026-05-10T17:00:00Z",
"completed": false,
"priority": 2,
"tags": ["work", "reports"]
}
```

### with_due_date_only

```json
{
"title": "Pay rent",
"dueDate": "2026-05-01T12:00:00Z"
}
```

### with_tags

```json
{
"title": "Plan team offsite",
"description": "Choose date and venue, prepare agenda.",
"tags": ["team", "planning", "events"]
}
```

### invalid_missing_title

```json
{
"description": "This body is invalid because title is required."
}
```
40 changes: 34 additions & 6 deletions internal/domain/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,32 @@ const (
StatusDone Status = "done"
)

type RepeatInterval string

const (
RepeatNever RepeatInterval = "never"
RepeatDaily RepeatInterval = "daily"
RepeatMonthly RepeatInterval = "monthly"
RepeatCustom RepeatInterval = "custom"
RepeatEven RepeatInterval = "even_days"
RepeatOdd RepeatInterval = "odd_days"
)

type RepeatConfig struct {
DailyInterval *int `json:"daily_interval,omitempty"`
MonthDay *int `json:"month_day,omitempty"`
SpecificDates []time.Time `json:"specific_dates,omitempty"`
}

type Task struct {
ID int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Status Status `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Repeat RepeatInterval `json:"repeat"`
Config *RepeatConfig `json:"config,omitempty"`
Status Status `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

func (s Status) Valid() bool {
Expand All @@ -27,3 +46,12 @@ func (s Status) Valid() bool {
return false
}
}

func (r RepeatInterval) Valid() bool {
switch r {
case RepeatNever, RepeatDaily, RepeatMonthly, RepeatCustom, RepeatEven, RepeatOdd:
return true
default:
return false
}
}
26 changes: 16 additions & 10 deletions internal/repository/postgres/task_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ func New(pool *pgxpool.Pool) *Repository {

func (r *Repository) Create(ctx context.Context, task *taskdomain.Task) (*taskdomain.Task, error) {
const query = `
INSERT INTO tasks (title, description, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, title, description, status, created_at, updated_at
INSERT INTO tasks (title, description, status, repeat, repeat_config, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, title, description, status, repeat, repeat_config, created_at, updated_at
`

row := r.pool.QueryRow(ctx, query, task.Title, task.Description, task.Status, task.CreatedAt, task.UpdatedAt)
row := r.pool.QueryRow(ctx, query, task.Title, task.Description, task.Status, task.Repeat, task.Config, task.CreatedAt, task.UpdatedAt)
created, err := scanTask(row)
if err != nil {
return nil, err
Expand All @@ -36,7 +36,7 @@ func (r *Repository) Create(ctx context.Context, task *taskdomain.Task) (*taskdo

func (r *Repository) GetByID(ctx context.Context, id int64) (*taskdomain.Task, error) {
const query = `
SELECT id, title, description, status, created_at, updated_at
SELECT id, title, description, status, repeat, repeat_config, created_at, updated_at
FROM tasks
WHERE id = $1
`
Expand All @@ -60,12 +60,14 @@ func (r *Repository) Update(ctx context.Context, task *taskdomain.Task) (*taskdo
SET title = $1,
description = $2,
status = $3,
updated_at = $4
WHERE id = $5
RETURNING id, title, description, status, created_at, updated_at
repeat = $4,
repeat_config = $5,
updated_at = $6
WHERE id = $7
RETURNING id, title, description, status, repeat, repeat_config, created_at, updated_at
`

row := r.pool.QueryRow(ctx, query, task.Title, task.Description, task.Status, task.UpdatedAt, task.ID)
row := r.pool.QueryRow(ctx, query, task.Title, task.Description, task.Status, task.Repeat, task.Config, task.UpdatedAt, task.ID)
updated, err := scanTask(row)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
Expand Down Expand Up @@ -95,7 +97,7 @@ func (r *Repository) Delete(ctx context.Context, id int64) error {

func (r *Repository) List(ctx context.Context) ([]taskdomain.Task, error) {
const query = `
SELECT id, title, description, status, created_at, updated_at
SELECT id, title, description, status, repeat, repeat_config, created_at, updated_at
FROM tasks
ORDER BY id DESC
`
Expand Down Expand Up @@ -131,20 +133,24 @@ func scanTask(scanner taskScanner) (*taskdomain.Task, error) {
var (
task taskdomain.Task
status string
repeat string
)

if err := scanner.Scan(
&task.ID,
&task.Title,
&task.Description,
&status,
&repeat,
&task.Config,
&task.CreatedAt,
&task.UpdatedAt,
); err != nil {
return nil, err
}

task.Status = taskdomain.Status(status)
task.Repeat = taskdomain.RepeatInterval(repeat)

return &task, nil
}
48 changes: 48 additions & 0 deletions internal/transport/http/docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,22 @@
"type": "string",
"enum": ["new", "in_progress", "done"],
"example": "new"
},
"repeat": {
"type": "string",
"enum": ["never", "daily", "monthly", "custom", "even_days", "odd_days"],
"example": "daily"
},
"config": {
"type": "object",
"properties": {
"daily_interval": { "type": "integer" },
"month_day": { "type": "integer" },
"specific_dates": {
"type": "array",
"items": { "type": "string", "format": "date-time" }
}
}
}
}
},
Expand All @@ -188,6 +204,22 @@
"type": "string",
"enum": ["new", "in_progress", "done"],
"example": "in_progress"
},
"repeat": {
"type": "string",
"enum": ["never", "daily", "monthly", "custom", "even_days", "odd_days"],
"example": "daily"
},
"config": {
"type": "object",
"properties": {
"daily_interval": { "type": "integer" },
"month_day": { "type": "integer" },
"specific_dates": {
"type": "array",
"items": { "type": "string", "format": "date-time" }
}
}
}
}
},
Expand All @@ -205,6 +237,22 @@
"enum": ["new", "in_progress", "done"],
"example": "new"
},
"repeat": {
"type": "string",
"enum": ["never", "daily", "monthly", "custom", "even_days", "odd_days"],
"example": "never"
},
"config": {
"type": "object",
"properties": {
"daily_interval": { "type": "integer" },
"month_day": { "type": "integer" },
"specific_dates": {
"type": "array",
"items": { "type": "string", "format": "date-time" }
}
}
},
"created_at": {
"type": "string",
"format": "date-time",
Expand Down
24 changes: 15 additions & 9 deletions internal/transport/http/handlers/dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,22 @@ import (
)

type taskMutationDTO struct {
Title string `json:"title"`
Description string `json:"description"`
Status taskdomain.Status `json:"status"`
Title string `json:"title"`
Description string `json:"description"`
Status taskdomain.Status `json:"status"`
Repeat taskdomain.RepeatInterval `json:"repeat,omitempty"`
Config *taskdomain.RepeatConfig `json:"config,omitempty"`
}

type taskDTO struct {
ID int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Status taskdomain.Status `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Status taskdomain.Status `json:"status"`
Repeat taskdomain.RepeatInterval `json:"repeat,omitempty"`
Config *taskdomain.RepeatConfig `json:"config,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

func newTaskDTO(task *taskdomain.Task) taskDTO {
Expand All @@ -27,6 +31,8 @@ func newTaskDTO(task *taskdomain.Task) taskDTO {
Title: task.Title,
Description: task.Description,
Status: task.Status,
Repeat: task.Repeat,
Config: task.Config,
CreatedAt: task.CreatedAt,
UpdatedAt: task.UpdatedAt,
}
Expand Down
4 changes: 4 additions & 0 deletions internal/transport/http/handlers/task_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ func (h *TaskHandler) Create(w http.ResponseWriter, r *http.Request) {
Title: req.Title,
Description: req.Description,
Status: req.Status,
Repeat: req.Repeat,
Config: req.Config,
})
if err != nil {
writeUsecaseError(w, err)
Expand Down Expand Up @@ -73,6 +75,8 @@ func (h *TaskHandler) Update(w http.ResponseWriter, r *http.Request) {
Title: req.Title,
Description: req.Description,
Status: req.Status,
Repeat: req.Repeat,
Config: req.Config,
})
if err != nil {
writeUsecaseError(w, err)
Expand Down
4 changes: 4 additions & 0 deletions internal/usecase/task/ports.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@ type CreateInput struct {
Title string
Description string
Status taskdomain.Status
Repeat taskdomain.RepeatInterval
Config *taskdomain.RepeatConfig
}

type UpdateInput struct {
Title string
Description string
Status taskdomain.Status
Repeat taskdomain.RepeatInterval
Config *taskdomain.RepeatConfig
}
20 changes: 20 additions & 0 deletions internal/usecase/task/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ func (s *Service) Create(ctx context.Context, input CreateInput) (*taskdomain.Ta
Title: normalized.Title,
Description: normalized.Description,
Status: normalized.Status,
Repeat: normalized.Repeat,
Config: normalized.Config,
}
now := s.now()
model.CreatedAt = now
Expand Down Expand Up @@ -67,6 +69,8 @@ func (s *Service) Update(ctx context.Context, id int64, input UpdateInput) (*tas
Title: normalized.Title,
Description: normalized.Description,
Status: normalized.Status,
Repeat: normalized.Repeat,
Config: normalized.Config,
UpdatedAt: s.now(),
}

Expand Down Expand Up @@ -106,6 +110,14 @@ func validateCreateInput(input CreateInput) (CreateInput, error) {
return CreateInput{}, fmt.Errorf("%w: invalid status", ErrInvalidInput)
}

if input.Repeat == "" {
input.Repeat = taskdomain.RepeatNever
}

if !input.Repeat.Valid() {
return CreateInput{}, fmt.Errorf("%w: invalid repeat type", ErrInvalidInput)
}

return input, nil
}

Expand All @@ -121,5 +133,13 @@ func validateUpdateInput(input UpdateInput) (UpdateInput, error) {
return UpdateInput{}, fmt.Errorf("%w: invalid status", ErrInvalidInput)
}

if input.Repeat == "" {
input.Repeat = taskdomain.RepeatNever
}

if !input.Repeat.Valid() {
return UpdateInput{}, fmt.Errorf("%w: invalid repeat type", ErrInvalidInput)
}

return input, nil
}