Skip to content

Commit 93bf8e6

Browse files
committed
feat: add update functionality for commands in CommandRepository
1 parent 234e3c3 commit 93bf8e6

File tree

12 files changed

+297
-29
lines changed

12 files changed

+297
-29
lines changed

internal/common/pagination/pagination.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
)
1212

1313
const (
14-
DefaultLimit = 2
14+
DefaultLimit = 10
1515
MaxLimit = 10
1616
)
1717

internal/db/command_repository.go

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"time"
99

1010
"github.com/google/uuid"
11+
"github.com/jackc/pgx/v5"
1112
"github.com/jackc/pgx/v5/pgconn"
1213
"github.com/jackc/pgx/v5/pgxpool"
1314
"github.com/raphico/go-device-telemetry-api/internal/common/pagination"
@@ -54,7 +55,7 @@ func (r *CommandRepository) Create(ctx context.Context, c *command.Command) erro
5455
return fmt.Errorf("failed to insert command: %w", err)
5556
}
5657

57-
if err := c.UpdateStatus(status); err != nil {
58+
if err := c.Status.SetStatus(status); err != nil {
5859
return fmt.Errorf("corrupt status: %w", err)
5960
}
6061

@@ -141,3 +142,84 @@ func (r *CommandRepository) FindCommands(
141142

142143
return result, nextCur, nil
143144
}
145+
146+
func (r *CommandRepository) FindById(
147+
ctx context.Context,
148+
id command.CommandID,
149+
deviceID device.DeviceID,
150+
) (*command.Command, error) {
151+
var (
152+
commandID uuid.UUID
153+
dbDeviceID uuid.UUID
154+
commandName string
155+
payload []byte
156+
status string
157+
executedAt *time.Time
158+
createdAt time.Time
159+
)
160+
161+
query := `
162+
SELECT id, device_id, command_name, payload, status, executed_at, created_at
163+
FROM commands
164+
WHERE id = $1 AND device_id = $2
165+
`
166+
167+
err := r.db.QueryRow(ctx, query, id, deviceID).Scan(
168+
&commandID,
169+
&dbDeviceID,
170+
&commandName,
171+
&payload,
172+
&status,
173+
&executedAt,
174+
&createdAt,
175+
)
176+
177+
if err != nil {
178+
if errors.Is(err, pgx.ErrNoRows) {
179+
return nil, command.ErrCommandNotFound
180+
}
181+
182+
return nil, fmt.Errorf("failed to find command by id: %w", err)
183+
}
184+
185+
return command.RehydrateCommand(
186+
commandID,
187+
dbDeviceID,
188+
commandName,
189+
payload,
190+
status,
191+
executedAt,
192+
createdAt,
193+
)
194+
}
195+
196+
func (r *CommandRepository) UpdateStatus(ctx context.Context, c *command.Command) error {
197+
if !c.ExecutedAt.Valid() {
198+
return fmt.Errorf("invalid executed_at")
199+
}
200+
201+
query := `
202+
UPDATE commands
203+
SET status = $1, executed_at = $2
204+
WHERE id = $3 AND device_id = $4
205+
`
206+
207+
tag, err := r.db.Exec(
208+
ctx,
209+
query,
210+
c.Status.String(),
211+
c.ExecutedAt.Time(),
212+
c.ID,
213+
c.DeviceID,
214+
)
215+
216+
if err != nil {
217+
return fmt.Errorf("failed to update command: %w", err)
218+
}
219+
220+
if tag.RowsAffected() == 0 {
221+
return command.ErrCommandNotFound
222+
}
223+
224+
return nil
225+
}

internal/db/device_repository.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,11 @@ func (r *DeviceRepository) Create(ctx context.Context, dev *device.Device) error
6565
func (r *DeviceRepository) FindById(
6666
ctx context.Context,
6767
id device.DeviceID,
68-
userId user.UserID,
68+
userID user.UserID,
6969
) (*device.Device, error) {
7070
var (
7171
deviceID uuid.UUID
72-
userID uuid.UUID
72+
dbUserID uuid.UUID
7373
name string
7474
deviceType string
7575
status string
@@ -84,7 +84,7 @@ func (r *DeviceRepository) FindById(
8484
WHERE id = $1 AND user_id = $2
8585
`
8686

87-
err := r.db.QueryRow(ctx, query, id, userId).Scan(
87+
err := r.db.QueryRow(ctx, query, id, dbUserID).Scan(
8888
&deviceID,
8989
&userID,
9090
&name,
@@ -100,7 +100,7 @@ func (r *DeviceRepository) FindById(
100100
return nil, device.ErrDeviceNotFound
101101
}
102102

103-
return nil, fmt.Errorf("find device by id failed: %w", err)
103+
return nil, fmt.Errorf("failed to find device by id: %w", err)
104104
}
105105

106106
return device.RehydrateDevice(

internal/domain/command/command.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type Command struct {
1515
Name Name
1616
Payload Payload
1717
Status Status
18-
ExecutedAt *time.Time
18+
ExecutedAt ExecutedAt
1919
CreatedAt time.Time
2020
}
2121

@@ -31,13 +31,12 @@ func NewCommand(
3131
}
3232
}
3333

34-
func (c *Command) UpdateStatus(value string) error {
35-
status, err := NewStatus(value)
36-
if err != nil {
37-
return err
38-
}
34+
func (c *Command) UpdateStatus(status Status) {
3935
c.Status = status
40-
return nil
36+
}
37+
38+
func (c *Command) UpdateExecutedAt(executedAt ExecutedAt) {
39+
c.ExecutedAt = executedAt
4140
}
4241

4342
func RehydrateCommand(
@@ -64,13 +63,24 @@ func RehydrateCommand(
6463
return nil, fmt.Errorf("corrupt status: %w", err)
6564
}
6665

66+
var execAt ExecutedAt
67+
if executedAt != nil {
68+
e, err := ExecutedAtFromTime(*executedAt)
69+
if err != nil {
70+
return nil, fmt.Errorf("corrupt executed_at: %w", err)
71+
}
72+
execAt = e
73+
} else {
74+
execAt = ExecutedAt{valid: false}
75+
}
76+
6777
return &Command{
6878
ID: CommandID(id),
6979
DeviceID: device.DeviceID(deviceID),
7080
Name: n,
7181
Payload: payload,
7282
Status: s,
73-
ExecutedAt: executedAt,
83+
ExecutedAt: execAt,
7484
CreatedAt: createdAt,
7585
}, nil
7686
}

internal/domain/command/errors.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package command
2+
3+
import "errors"
4+
5+
var (
6+
ErrCommandNotFound = errors.New("command not found")
7+
)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package command
2+
3+
import (
4+
"errors"
5+
"strings"
6+
"time"
7+
)
8+
9+
type ExecutedAt struct {
10+
value time.Time
11+
valid bool
12+
}
13+
14+
func NewExecutedAt(raw string) (ExecutedAt, error) {
15+
now := time.Now().UTC()
16+
17+
raw = strings.TrimSpace(raw)
18+
if raw == "" {
19+
return ExecutedAt{}, errors.New("command executed_at is required")
20+
}
21+
22+
t, err := time.Parse(time.RFC3339, raw)
23+
if err != nil || t.IsZero() {
24+
return ExecutedAt{}, errors.New("invalid executed_at")
25+
}
26+
27+
if t.After(now.Add(5 * time.Minute)) { // reject suspicious future values
28+
return ExecutedAt{}, errors.New("command executed_at cannot be in the future")
29+
}
30+
31+
return ExecutedAt{value: t.UTC(), valid: true}, nil
32+
}
33+
34+
func ExecutedAtFromTime(t time.Time) (ExecutedAt, error) {
35+
if t.IsZero() {
36+
return ExecutedAt{valid: false}, nil // NULL
37+
}
38+
39+
now := time.Now().UTC()
40+
if t.After(now.Add(5 * time.Minute)) {
41+
return ExecutedAt{}, errors.New("command executed_at cannot be in the future")
42+
}
43+
44+
return ExecutedAt{value: t.UTC(), valid: true}, nil
45+
}
46+
47+
func (e ExecutedAt) Time() time.Time {
48+
return e.value
49+
}
50+
51+
func (e ExecutedAt) Valid() bool {
52+
return e.valid
53+
}

internal/domain/command/repository.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,10 @@ type Repository interface {
1515
limit int,
1616
cursor *pagination.Cursor,
1717
) ([]*Command, *pagination.Cursor, error)
18+
FindById(
19+
ctx context.Context,
20+
id CommandID,
21+
deviceID device.DeviceID,
22+
) (*Command, error)
23+
UpdateStatus(ctx context.Context, c *Command) error
1824
}

internal/domain/command/service.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ func (s *Service) CreateCommand(
2121
name Name,
2222
payload Payload,
2323
) (*Command, error) {
24-
command := NewCommand(deviceID, name, payload)
24+
cmd := NewCommand(deviceID, name, payload)
2525

26-
err := s.repo.Create(ctx, command)
26+
err := s.repo.Create(ctx, cmd)
2727
if err != nil {
2828
return nil, err
2929
}
3030

31-
return command, nil
31+
return cmd, nil
3232
}
3333

3434
func (s *Service) ListDeviceCommands(
@@ -39,3 +39,26 @@ func (s *Service) ListDeviceCommands(
3939
) ([]*Command, *pagination.Cursor, error) {
4040
return s.repo.FindCommands(ctx, deviceID, limit, cursor)
4141
}
42+
43+
func (s *Service) UpdateCommandStatus(
44+
ctx context.Context,
45+
id CommandID,
46+
deviceID device.DeviceID,
47+
status Status,
48+
executedAt ExecutedAt,
49+
) (*Command, error) {
50+
cmd, err := s.repo.FindById(ctx, id, deviceID)
51+
if err != nil {
52+
return nil, err
53+
}
54+
55+
cmd.UpdateStatus(status)
56+
cmd.UpdateExecutedAt(executedAt)
57+
58+
err = s.repo.UpdateStatus(ctx, cmd)
59+
if err != nil {
60+
return nil, err
61+
}
62+
63+
return cmd, nil
64+
}

internal/domain/command/status.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,12 @@ func NewStatus(value string) (Status, error) {
2828
func (s Status) String() string {
2929
return s.value
3030
}
31+
32+
func (s *Status) SetStatus(value string) error {
33+
status, err := NewStatus(value)
34+
if err != nil {
35+
return err
36+
}
37+
*s = status
38+
return nil
39+
}

0 commit comments

Comments
 (0)