Skip to content

Commit 43098a4

Browse files
committed
feat: implement get device by id
1 parent fb7cf68 commit 43098a4

File tree

9 files changed

+164
-17
lines changed

9 files changed

+164
-17
lines changed

internal/db/device_repository.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8+
"time"
89

10+
"github.com/google/uuid"
11+
"github.com/jackc/pgx/v5"
912
"github.com/jackc/pgx/v5/pgconn"
1013
"github.com/jackc/pgx/v5/pgxpool"
1114
"github.com/raphico/go-device-telemetry-api/internal/domain/device"
@@ -57,3 +60,43 @@ func (r *DeviceRepository) Create(ctx context.Context, device *device.Device) er
5760

5861
return nil
5962
}
63+
64+
func (r *DeviceRepository) FindById(ctx context.Context, id device.DeviceID, userId user.UserID) (*device.Device, error) {
65+
var (
66+
deviceID uuid.UUID
67+
userID uuid.UUID
68+
name string
69+
deviceType string
70+
status string
71+
metadata []byte
72+
createdAt time.Time
73+
updatedAt time.Time
74+
)
75+
76+
query := `
77+
SELECT id, user_id, name, device_type, status, metadata, created_at, updated_at
78+
FROM devices
79+
WHERE id = $1 AND user_id = $2
80+
`
81+
82+
err := r.db.QueryRow(ctx, query, id, userId).Scan(
83+
&deviceID,
84+
&userID,
85+
&name,
86+
&deviceType,
87+
&status,
88+
&metadata,
89+
&createdAt,
90+
&updatedAt,
91+
)
92+
93+
if err != nil {
94+
if errors.Is(err, pgx.ErrNoRows) {
95+
return nil, device.ErrDeviceNotFound
96+
}
97+
98+
return nil, fmt.Errorf("find device by id failed: %w", err)
99+
}
100+
101+
return device.RehydrateDevice(deviceID, userID, name, deviceType, status, metadata, createdAt, updatedAt)
102+
}

internal/db/token_repository.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"time"
88

9+
"github.com/google/uuid"
910
"github.com/jackc/pgx/v5"
1011
"github.com/jackc/pgx/v5/pgconn"
1112
"github.com/jackc/pgx/v5/pgxpool"
@@ -60,7 +61,7 @@ func (r *TokenRepository) Create(ctx context.Context, t *token.Token) error {
6061

6162
func (r *TokenRepository) FindValidTokenByHash(ctx context.Context, hash []byte, scope string) (*token.Token, error) {
6263
var (
63-
id token.TokenID
64+
id uuid.UUID
6465
dbHash []byte
6566
userID user.UserID
6667
dbScope string

internal/domain/device/device.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package device
22

33
import (
4+
"encoding/json"
5+
"fmt"
46
"time"
57

8+
"github.com/google/uuid"
69
"github.com/raphico/go-device-telemetry-api/internal/domain/user"
710
)
811

@@ -45,3 +48,50 @@ func NewDevice(userId user.UserID, name, status, deviceType string, metadata map
4548
Metadata: metadata,
4649
}, nil
4750
}
51+
52+
func RehydrateDevice(
53+
id uuid.UUID,
54+
userID uuid.UUID,
55+
name string,
56+
deviceType string,
57+
status string,
58+
metadataBytes []byte,
59+
createdAt time.Time,
60+
updatedAt time.Time,
61+
) (*Device, error) {
62+
n, err := NewName(name)
63+
if err != nil {
64+
return nil, fmt.Errorf("corrupt device name: %w", err)
65+
}
66+
67+
s, err := NewStatus(status)
68+
if err != nil {
69+
return nil, fmt.Errorf("corrupt device status: %w", err)
70+
}
71+
72+
dt, err := NewDeviceType(deviceType)
73+
if err != nil {
74+
return nil, fmt.Errorf("corrupt device type: %w", err)
75+
}
76+
77+
var metadata map[string]any
78+
if metadataBytes == nil {
79+
metadata = make(map[string]any)
80+
} else {
81+
err := json.Unmarshal(metadataBytes, &metadata)
82+
if err != nil {
83+
return nil, fmt.Errorf("corrupt metadata: %w", err)
84+
}
85+
}
86+
87+
return &Device{
88+
ID: DeviceID(id),
89+
UserID: user.UserID(userID),
90+
Name: n,
91+
Status: s,
92+
DeviceType: dt,
93+
Metadata: metadata,
94+
UpdatedAt: updatedAt,
95+
CreatedAt: createdAt,
96+
}, nil
97+
}

internal/domain/device/errors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package device
33
import "errors"
44

55
var (
6+
ErrDeviceNotFound = errors.New("device not found")
67
ErrNameRequired = errors.New("device name is required")
78
ErrNameTooShort = errors.New("device name must be at least 3 characters")
89
ErrNameTooLong = errors.New("device name must be at most 50 characters")
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
package device
22

3-
import "context"
3+
import (
4+
"context"
5+
6+
"github.com/raphico/go-device-telemetry-api/internal/domain/user"
7+
)
48

59
type Repository interface {
610
Create(ctx context.Context, device *Device) error
11+
FindById(ctx context.Context, id DeviceID, userId user.UserID) (*Device, error)
712
}

internal/domain/device/service.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,12 @@ func (s *Service) CreateDevice(
3535

3636
return device, nil
3737
}
38+
39+
func (s *Service) GetDevice(ctx context.Context, id DeviceID, userId user.UserID) (*Device, error) {
40+
device, err := s.repo.FindById(ctx, id, userId)
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
return device, nil
46+
}

internal/domain/token/token.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func HashPlaintext(plaintext string) []byte {
5050
}
5151

5252
func RehydrateToken(
53-
id TokenID,
53+
id uuid.UUID,
5454
hash []byte,
5555
userID user.UserID,
5656
scope string,
@@ -60,7 +60,7 @@ func RehydrateToken(
6060
createdAt time.Time,
6161
) *Token {
6262
return &Token{
63-
ID: id,
63+
ID: TokenID(id),
6464
Hash: hash,
6565
UserID: userID,
6666
Scope: scope,

internal/http/device_handler.go

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,22 @@ import (
66
"fmt"
77
"net/http"
88

9+
"github.com/go-chi/chi/v5"
10+
"github.com/google/uuid"
911
"github.com/raphico/go-device-telemetry-api/internal/domain/device"
1012
"github.com/raphico/go-device-telemetry-api/internal/domain/user"
1113
"github.com/raphico/go-device-telemetry-api/internal/logger"
1214
)
1315

1416
type DeviceHandler struct {
15-
log *logger.Logger
16-
service *device.Service
17+
log *logger.Logger
18+
device *device.Service
1719
}
1820

1921
func NewDeviceHandler(log *logger.Logger, deviceService *device.Service) *DeviceHandler {
2022
return &DeviceHandler{
21-
log: log,
22-
service: deviceService,
23+
log: log,
24+
device: deviceService,
2325
}
2426
}
2527

@@ -30,7 +32,7 @@ type createDeviceRequest struct {
3032
Metadata map[string]any `json:"metadata"`
3133
}
3234

33-
type createDeviceResponse struct {
35+
type deviceResponse struct {
3436
ID string `json:"id"`
3537
Name string `json:"name"`
3638
DeviceType string `json:"device_type"`
@@ -52,7 +54,7 @@ func (h *DeviceHandler) HandleCreateDevice(w http.ResponseWriter, r *http.Reques
5254
WriteUnauthorizedError(w)
5355
}
5456

55-
d, err := h.service.CreateDevice(r.Context(), userId, req.Name, req.Status, req.DeviceType, req.Metadata)
57+
dev, err := h.device.CreateDevice(r.Context(), userId, req.Name, req.Status, req.DeviceType, req.Metadata)
5658
if err != nil {
5759
switch {
5860
case errors.Is(err, device.ErrNameRequired),
@@ -76,17 +78,52 @@ func (h *DeviceHandler) HandleCreateDevice(w http.ResponseWriter, r *http.Reques
7678
}
7779
}
7880

79-
res := createDeviceResponse{
80-
ID: d.ID.String(),
81-
Name: d.Name.String(),
82-
DeviceType: string(d.DeviceType),
83-
Status: string(d.Status),
84-
Metadata: d.Metadata,
81+
res := deviceResponse{
82+
ID: dev.ID.String(),
83+
Name: dev.Name.String(),
84+
DeviceType: string(dev.DeviceType),
85+
Status: string(dev.Status),
86+
Metadata: dev.Metadata,
8587
}
8688

8789
WriteJSON(w, http.StatusCreated, res, nil)
8890
}
8991

90-
func (h *DeviceHandler) HandleGetDeviceById(w http.ResponseWriter, r *http.Request) {
92+
func (h *DeviceHandler) HandleGetDevice(w http.ResponseWriter, r *http.Request) {
93+
idStr := chi.URLParam(r, "id")
9194

95+
id, err := uuid.Parse(idStr)
96+
if err != nil {
97+
WriteJSONError(w, http.StatusBadRequest, invalidRequest, "invalid device id")
98+
return
99+
}
100+
101+
userId, ok := GetUserID(r.Context())
102+
if !ok {
103+
h.log.Debug(fmt.Sprint("missing user id in context", "path", r.URL.Path))
104+
WriteUnauthorizedError(w)
105+
return
106+
}
107+
108+
dev, err := h.device.GetDevice(r.Context(), device.DeviceID(id), userId)
109+
if err != nil {
110+
switch {
111+
case errors.Is(err, device.ErrDeviceNotFound):
112+
WriteJSONError(w, http.StatusBadRequest, invalidRequest, "device not found")
113+
default:
114+
h.log.Error(fmt.Sprintf("failed to get device: %v", err))
115+
WriteInternalError(w)
116+
}
117+
return
118+
}
119+
120+
res := deviceResponse{
121+
ID: dev.ID.String(),
122+
Name: dev.Name.String(),
123+
DeviceType: string(dev.DeviceType),
124+
Status: string(dev.Status),
125+
Metadata: dev.Metadata,
126+
}
127+
128+
WriteJSON(w, http.StatusOK, res, nil)
92129
}

internal/http/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func NewRouter(
3838

3939
r.Route("/devices", func(r chi.Router) {
4040
r.Post("/", deviceHandler.HandleCreateDevice)
41+
r.Get("/{id}", deviceHandler.HandleGetDevice)
4142
})
4243
})
4344

0 commit comments

Comments
 (0)