Skip to content

Commit 64ade49

Browse files
committed
feat: add AddTodoListItem command
1 parent a5e5d14 commit 64ade49

File tree

5 files changed

+182
-12
lines changed

5 files changed

+182
-12
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package command
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/get-eventually/go-eventually/core/command"
9+
"github.com/get-eventually/go-eventually/examples/todolist/domain/todolist"
10+
)
11+
12+
type AddTodoListItem struct {
13+
TodoListID todolist.ID
14+
TodoItemID todolist.ItemID
15+
Title string
16+
Description string
17+
DueDate time.Time
18+
}
19+
20+
// Name implements command.Command
21+
func (AddTodoListItem) Name() string { return "AddTodoListItem" }
22+
23+
var _ command.Handler[AddTodoListItem] = AddTodoListItemHandler{}
24+
25+
type AddTodoListItemHandler struct {
26+
Clock func() time.Time
27+
Repository todolist.Repository
28+
}
29+
30+
// Handle implements command.Handler
31+
func (h AddTodoListItemHandler) Handle(ctx context.Context, cmd command.Envelope[AddTodoListItem]) error {
32+
todoList, err := h.Repository.Get(ctx, cmd.Message.TodoListID)
33+
if err != nil {
34+
return fmt.Errorf("command.AddTodoListItem: failed to get TodoList from repository, %w", err)
35+
}
36+
37+
now := h.Clock()
38+
39+
if err := todoList.AddItem(
40+
cmd.Message.TodoItemID,
41+
cmd.Message.Title,
42+
cmd.Message.Description,
43+
cmd.Message.DueDate,
44+
now,
45+
); err != nil {
46+
return fmt.Errorf("command.AddTodoListItem: failed to add item to TodoList, %w", err)
47+
}
48+
49+
if err := h.Repository.Save(ctx, todoList); err != nil {
50+
return fmt.Errorf("command.AddTodoListItem: failed to save new TodoList version, %w", err)
51+
}
52+
53+
return nil
54+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package command_test
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/get-eventually/go-eventually/core/aggregate"
8+
"github.com/get-eventually/go-eventually/core/command"
9+
"github.com/get-eventually/go-eventually/core/event"
10+
"github.com/get-eventually/go-eventually/core/test/scenario"
11+
appcommand "github.com/get-eventually/go-eventually/examples/todolist/command"
12+
"github.com/get-eventually/go-eventually/examples/todolist/domain/todolist"
13+
"github.com/google/uuid"
14+
)
15+
16+
func TestAddTodoListItem(t *testing.T) {
17+
now := time.Now()
18+
commandHandlerFactory := func(es event.Store) appcommand.AddTodoListItemHandler {
19+
return appcommand.AddTodoListItemHandler{
20+
Clock: func() time.Time { return now },
21+
Repository: aggregate.NewEventSourcedRepository(es, todolist.Type),
22+
}
23+
}
24+
25+
todoListID := todolist.ID(uuid.New())
26+
todoItemID := todolist.ItemID(uuid.New())
27+
listTitle := "my list"
28+
listOwner := "me"
29+
30+
t.Run("it fails when the target TodoList does not exist", func(t *testing.T) {
31+
scenario.CommandHandler[appcommand.AddTodoListItem, appcommand.AddTodoListItemHandler]().
32+
When(command.ToEnvelope(appcommand.AddTodoListItem{
33+
TodoListID: todoListID,
34+
TodoItemID: todoItemID,
35+
Title: "a todo item that should fail",
36+
})).
37+
ThenError(aggregate.ErrRootNotFound).
38+
AssertOn(t, commandHandlerFactory)
39+
})
40+
41+
t.Run("it works", func(t *testing.T) {
42+
scenario.CommandHandler[appcommand.AddTodoListItem, appcommand.AddTodoListItemHandler]().
43+
Given(event.Persisted{
44+
StreamID: event.StreamID(todoListID.String()),
45+
Version: 1,
46+
Envelope: event.ToEnvelope(todolist.WasCreated{
47+
ID: todoListID,
48+
Title: listTitle,
49+
Owner: listOwner,
50+
CreationTime: now.Add(-2 * time.Minute),
51+
}),
52+
}).
53+
When(command.ToEnvelope(appcommand.AddTodoListItem{
54+
TodoListID: todoListID,
55+
TodoItemID: todoItemID,
56+
Title: "a todo item that should succeed",
57+
})).
58+
Then(event.Persisted{
59+
StreamID: event.StreamID(todoListID.String()),
60+
Version: 2,
61+
Envelope: event.ToEnvelope(todolist.ItemWasAdded{
62+
ID: todoItemID,
63+
Title: "a todo item that should succeed",
64+
CreationTime: now,
65+
}),
66+
}).
67+
AssertOn(t, commandHandlerFactory)
68+
})
69+
70+
}

examples/todolist/domain/todolist/event.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ type WasCreated struct {
1212
func (WasCreated) Name() string { return "TodoListWasCreated" }
1313

1414
type ItemWasAdded struct {
15-
ID ItemID
16-
Title string
17-
Description string
18-
DueDate time.Time
15+
ID ItemID
16+
Title string
17+
Description string
18+
DueDate time.Time
19+
CreationTime time.Time
1920
}
2021

2122
func (ItemWasAdded) Name() string { return "TodoListItemWasAdded" }

examples/todolist/domain/todolist/item.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ func (id ItemID) String() string { return uuid.UUID(id).String() }
1616
type Item struct {
1717
aggregate.BaseRoot
1818

19-
id ItemID
20-
title string
21-
description string
22-
completed bool
23-
dueDate time.Time
19+
id ItemID
20+
title string
21+
description string
22+
completed bool
23+
dueDate time.Time
24+
creationTime time.Time
2425
}
2526

2627
func (item *Item) Apply(event event.Event) error {
@@ -31,6 +32,7 @@ func (item *Item) Apply(event event.Event) error {
3132
item.description = evt.Description
3233
item.completed = false
3334
item.dueDate = evt.DueDate
35+
item.creationTime = evt.CreationTime
3436

3537
case ItemMarkedAsDone:
3638
item.completed = true

examples/todolist/domain/todolist/todolist.go

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,12 @@ func (tl *TodoList) Apply(event event.Event) error {
9999
}
100100

101101
var (
102-
ErrEmptyID = errors.New("todolist.TodoList: empty id provided")
103-
ErrEmptyTitle = errors.New("todolist.TodoList: empty title provided")
104-
ErrNoOwnerSpecified = errors.New("todolist.TodoList: no owner specified")
102+
ErrEmptyID = errors.New("todolist.TodoList: empty id provided")
103+
ErrEmptyTitle = errors.New("todolist.TodoList: empty title provided")
104+
ErrNoOwnerSpecified = errors.New("todolist.TodoList: no owner specified")
105+
ErrEmptyItemID = errors.New("todolist.TodoList: empty item id provided")
106+
ErrEmptyItemTitle = errors.New("todolist.TodoList: empty item title provided")
107+
ErrItemAlreadyExists = errors.New("todolist.TodoList: item already exists")
105108
)
106109

107110
func Create(id ID, title, owner string, now time.Time) (*TodoList, error) {
@@ -134,3 +137,43 @@ func Create(id ID, title, owner string, now time.Time) (*TodoList, error) {
134137

135138
return &todoList, nil
136139
}
140+
141+
func (todoList *TodoList) itemByID(id ItemID) (*Item, bool) {
142+
for _, item := range todoList.items {
143+
if item.id == id {
144+
return item, true
145+
}
146+
}
147+
148+
return nil, false
149+
}
150+
151+
func (todoList *TodoList) AddItem(id ItemID, title, description string, dueDate, now time.Time) error {
152+
wrapErr := func(err error) error {
153+
return fmt.Errorf("todolist.AddItem: failed to add new TodoItem to list, %w", err)
154+
}
155+
156+
if uuid.UUID(id) == uuid.Nil {
157+
return wrapErr(ErrEmptyItemID)
158+
}
159+
160+
if title == "" {
161+
return wrapErr(ErrEmptyTitle)
162+
}
163+
164+
if _, ok := todoList.itemByID(id); ok {
165+
return wrapErr(ErrItemAlreadyExists)
166+
}
167+
168+
if err := aggregate.RecordThat[ID](todoList, event.ToEnvelope(ItemWasAdded{
169+
ID: id,
170+
Title: title,
171+
Description: description,
172+
DueDate: dueDate,
173+
CreationTime: now,
174+
})); err != nil {
175+
return fmt.Errorf("todolist.AddItem: failed to apply domain event, %w", err)
176+
}
177+
178+
return nil
179+
}

0 commit comments

Comments
 (0)