This commit is contained in:
Erik Winter 2024-09-11 07:33:22 +02:00
parent 4f21fd8aa8
commit 5c95750a0a
7 changed files with 100 additions and 50 deletions

View File

@ -7,6 +7,7 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"path" "path"
"slices"
"strings" "strings"
"time" "time"
) )
@ -66,8 +67,21 @@ func (s *Server) SyncGet(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
ks := make([]Kind, 0)
ksStr := r.URL.Query().Get("ks")
if ksStr != "" {
for _, k := range strings.Split(ksStr, ",") {
if !slices.Contains(KnownKinds, Kind(k)) {
msg := fmt.Sprintf("unknown kind: %s", k)
http.Error(w, fmtError(msg), http.StatusBadRequest)
s.logger.Info(msg)
return
}
ks = append(ks, Kind(k))
}
}
items, err := s.syncer.Updated(timestamp) items, err := s.syncer.Updated(ks, timestamp)
if err != nil { if err != nil {
msg := err.Error() msg := err.Error()
http.Error(w, fmtError(msg), http.StatusInternalServerError) http.Error(w, fmtError(msg), http.StatusInternalServerError)
@ -118,6 +132,12 @@ func (s *Server) SyncPost(w http.ResponseWriter, r *http.Request) {
s.logger.Info(msg) s.logger.Info(msg)
return return
} }
if !slices.Contains(KnownKinds, item.Kind) {
msg := fmt.Sprintf("items %s does not have a know kind", item.ID)
http.Error(w, fmtError(msg), http.StatusBadRequest)
s.logger.Info(msg)
return
}
if item.Body == "" { if item.Body == "" {
msg := fmt.Sprintf(`{"error":"item %s does not have a body"}`, item.ID) msg := fmt.Sprintf(`{"error":"item %s does not have a body"}`, item.ID)
http.Error(w, msg, http.StatusBadRequest) http.Error(w, msg, http.StatusBadRequest)

View File

@ -11,6 +11,7 @@ import (
"net/url" "net/url"
"os" "os"
"sort" "sort"
"strings"
"testing" "testing"
"time" "time"
) )
@ -56,9 +57,9 @@ func TestSyncGet(t *testing.T) {
mem := NewMemory() mem := NewMemory()
items := []Item{ items := []Item{
{ID: "id-0", Updated: now.Add(-10 * time.Minute)}, {ID: "id-0", Kind: KindEvent, Updated: now.Add(-10 * time.Minute)},
{ID: "id-1", Updated: now.Add(-5 * time.Minute)}, {ID: "id-1", Kind: KindEvent, Updated: now.Add(-5 * time.Minute)},
{ID: "id-2", Updated: now.Add(time.Minute)}, {ID: "id-2", Kind: KindTask, Updated: now.Add(time.Minute)},
} }
for _, item := range items { for _, item := range items {
@ -73,6 +74,7 @@ func TestSyncGet(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
name string name string
ts time.Time ts time.Time
ks []string
expStatus int expStatus int
expItems []Item expItems []Item
}{ }{
@ -82,14 +84,28 @@ func TestSyncGet(t *testing.T) {
expItems: items, expItems: items,
}, },
{ {
name: "normal", name: "new",
ts: now.Add(-6 * time.Minute), ts: now.Add(-6 * time.Minute),
expStatus: http.StatusOK, expStatus: http.StatusOK,
expItems: []Item{items[1], items[2]}, expItems: []Item{items[1], items[2]},
}, },
{
name: "kind",
ks: []string{string(KindTask)},
expStatus: http.StatusOK,
expItems: []Item{items[2]},
},
{
name: "unknown kind",
ks: []string{"test"},
expStatus: http.StatusBadRequest,
},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
url := fmt.Sprintf("/sync?ts=%s", url.QueryEscape(tc.ts.Format(time.RFC3339))) url := fmt.Sprintf("/sync?ts=%s", url.QueryEscape(tc.ts.Format(time.RFC3339)))
if len(tc.ks) > 0 {
url = fmt.Sprintf("%s&ks=%s", url, strings.Join(tc.ks, ","))
}
req, err := http.NewRequest(http.MethodGet, url, nil) req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil { if err != nil {
t.Errorf("exp nil, got %v", err) t.Errorf("exp nil, got %v", err)
@ -101,6 +117,10 @@ func TestSyncGet(t *testing.T) {
if res.Result().StatusCode != tc.expStatus { if res.Result().StatusCode != tc.expStatus {
t.Errorf("exp %v, got %v", tc.expStatus, res.Result().StatusCode) t.Errorf("exp %v, got %v", tc.expStatus, res.Result().StatusCode)
} }
if tc.expStatus != http.StatusOK {
return
}
var actItems []Item var actItems []Item
actBody, err := io.ReadAll(res.Result().Body) actBody, err := io.ReadAll(res.Result().Body)
if err != nil { if err != nil {
@ -149,20 +169,20 @@ func TestSyncPost(t *testing.T) {
{ {
name: "invalid item", name: "invalid item",
reqBody: []byte(`[ reqBody: []byte(`[
{"id":"id-1","kind":"test","updated":"2024-09-06T08:00:00Z"}, {"id":"id-1","kind":"event","updated":"2024-09-06T08:00:00Z"},
]`), ]`),
expStatus: http.StatusBadRequest, expStatus: http.StatusBadRequest,
}, },
{ {
name: "normal", name: "normal",
reqBody: []byte(`[ reqBody: []byte(`[
{"id":"id-1","kind":"test","updated":"2024-09-06T08:00:00Z","deleted":false,"body":"item"}, {"id":"id-1","kind":"event","updated":"2024-09-06T08:00:00Z","deleted":false,"body":"item"},
{"id":"id-2","kind":"test","updated":"2024-09-06T08:12:00Z","deleted":false,"body":"item2"} {"id":"id-2","kind":"event","updated":"2024-09-06T08:12:00Z","deleted":false,"body":"item2"}
]`), ]`),
expStatus: http.StatusNoContent, expStatus: http.StatusNoContent,
expItems: []Item{ expItems: []Item{
{ID: "id-1", Updated: time.Date(2024, 9, 6, 8, 0, 0, 0, time.UTC)}, {ID: "id-1", Kind: KindEvent, Updated: time.Date(2024, 9, 6, 8, 0, 0, 0, time.UTC)},
{ID: "id-2", Updated: time.Date(2024, 9, 6, 12, 0, 0, 0, time.UTC)}, {ID: "id-2", Kind: KindEvent, Updated: time.Date(2024, 9, 6, 12, 0, 0, 0, time.UTC)},
}, },
}, },
} { } {
@ -181,7 +201,7 @@ func TestSyncPost(t *testing.T) {
t.Errorf("exp %v, got %v", tc.expStatus, res.Result().StatusCode) t.Errorf("exp %v, got %v", tc.expStatus, res.Result().StatusCode)
} }
actItems, err := mem.Updated(time.Time{}) actItems, err := mem.Updated([]Kind{}, time.Time{})
if err != nil { if err != nil {
t.Errorf("exp nil, git %v", err) t.Errorf("exp nil, git %v", err)
} }

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"slices"
"time" "time"
) )
@ -20,11 +21,13 @@ func (m *Memory) Update(item Item) error {
return nil return nil
} }
func (m *Memory) Updated(timestamp time.Time) ([]Item, error) { func (m *Memory) Updated(kinds []Kind, timestamp time.Time) ([]Item, error) {
result := make([]Item, 0) result := make([]Item, 0)
for _, i := range m.items { for _, i := range m.items {
if timestamp.IsZero() || i.Updated.Equal(timestamp) || i.Updated.After(timestamp) { timeOK := timestamp.IsZero() || i.Updated.Equal(timestamp) || i.Updated.After(timestamp)
kindOK := len(kinds) == 0 || slices.Contains(kinds, i.Kind)
if timeOK && kindOK {
result = append(result, i) result = append(result, i)
} }
} }

View File

@ -11,7 +11,7 @@ func TestMemoryItem(t *testing.T) {
mem := NewMemory() mem := NewMemory()
t.Log("start empty") t.Log("start empty")
actItems, actErr := mem.Updated(time.Time{}) actItems, actErr := mem.Updated([]Kind{}, time.Time{})
if actErr != nil { if actErr != nil {
t.Errorf("exp nil, got %v", actErr) t.Errorf("exp nil, got %v", actErr)
} }
@ -20,11 +20,11 @@ func TestMemoryItem(t *testing.T) {
} }
t.Log("add one") t.Log("add one")
t1 := NewItem("test") t1 := NewItem(Kind("kinda"), "test")
if actErr := mem.Update(t1); actErr != nil { if actErr := mem.Update(t1); actErr != nil {
t.Errorf("exp nil, got %v", actErr) t.Errorf("exp nil, got %v", actErr)
} }
actItems, actErr = mem.Updated(time.Time{}) actItems, actErr = mem.Updated([]Kind{}, time.Time{})
if actErr != nil { if actErr != nil {
t.Errorf("exp nil, got %v", actErr) t.Errorf("exp nil, got %v", actErr)
} }
@ -38,11 +38,11 @@ func TestMemoryItem(t *testing.T) {
before := time.Now() before := time.Now()
t.Log("add second") t.Log("add second")
t2 := NewItem("test 2") t2 := NewItem(Kind("kindb"), "test 2")
if actErr := mem.Update(t2); actErr != nil { if actErr := mem.Update(t2); actErr != nil {
t.Errorf("exp nil, got %v", actErr) t.Errorf("exp nil, got %v", actErr)
} }
actItems, actErr = mem.Updated(time.Time{}) actItems, actErr = mem.Updated([]Kind{}, time.Time{})
if actErr != nil { if actErr != nil {
t.Errorf("exp nil, got %v", actErr) t.Errorf("exp nil, got %v", actErr)
} }
@ -56,7 +56,7 @@ func TestMemoryItem(t *testing.T) {
t.Errorf("exp %v, got %v", actItems[1].ID, t2.ID) t.Errorf("exp %v, got %v", actItems[1].ID, t2.ID)
} }
actItems, actErr = mem.Updated(before) actItems, actErr = mem.Updated([]Kind{}, before)
if actErr != nil { if actErr != nil {
t.Errorf("exp nil, got %v", actErr) t.Errorf("exp nil, got %v", actErr)
} }
@ -72,7 +72,7 @@ func TestMemoryItem(t *testing.T) {
if actErr := mem.Update(t1); actErr != nil { if actErr := mem.Update(t1); actErr != nil {
t.Errorf("exp nil, got %v", actErr) t.Errorf("exp nil, got %v", actErr)
} }
actItems, actErr = mem.Updated(before) actItems, actErr = mem.Updated([]Kind{}, before)
if actErr != nil { if actErr != nil {
t.Errorf("exp nil, got %v", actErr) t.Errorf("exp nil, got %v", actErr)
} }
@ -85,4 +85,16 @@ func TestMemoryItem(t *testing.T) {
if actItems[1].ID != t2.ID { if actItems[1].ID != t2.ID {
t.Errorf("exp %v, got %v", actItems[1].ID, t2.ID) t.Errorf("exp %v, got %v", actItems[1].ID, t2.ID)
} }
t.Log("select kind")
actItems, actErr = mem.Updated([]Kind{"kinda"}, time.Time{})
if actErr != nil {
t.Errorf("exp nil, got %v", actErr)
}
if len(actItems) != 1 {
t.Errorf("exp 1, got %d", len(actItems))
}
if actItems[0].ID != t1.ID {
t.Errorf("exp %v, got %v", t1.ID, actItems[0].ID)
}
} }

View File

@ -10,6 +10,11 @@ type Kind string
const ( const (
KindTask Kind = "task" KindTask Kind = "task"
KindEvent Kind = "event"
)
var (
KnownKinds = []Kind{KindTask, KindEvent}
) )
type Item struct { type Item struct {
@ -20,32 +25,11 @@ type Item struct {
Body string `json:"body"` Body string `json:"body"`
} }
func NewItem(body string) Item { func NewItem(k Kind, body string) Item {
return Item{ return Item{
ID: uuid.New().String(), ID: uuid.New().String(),
Kind: k,
Updated: time.Now(), Updated: time.Now(),
Body: body, Body: body,
} }
} }
type Task struct {
id string
description string
updated time.Time
}
func NewTask(description string) Task {
return Task{
id: uuid.New().String(),
description: description,
updated: time.Now(),
}
}
func (t Task) ID() string {
return t.id
}
func (t Task) Updated() time.Time {
return t.updated
}

View File

@ -67,14 +67,25 @@ body=?`,
return nil return nil
} }
func (s *Sqlite) Updated(t time.Time) ([]Item, error) { func (s *Sqlite) Updated(ks []Kind, t time.Time) ([]Item, error) {
rows, err := s.db.Query(` query := `
SELECT id, kind, updated, deleted, body SELECT id, kind, updated, deleted, body
FROM items FROM items
WHERE updated > ?`, t.Format(timestampFormat)) WHERE updated > ?`
var rows *sql.Rows
var err error
if len(ks) == 0 {
rows, err = s.db.Query(query, t.Format(timestampFormat))
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err) return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
} }
} else {
query = fmt.Sprintf("%s AND kind in (?)", query)
rows, err = s.db.Query(query, t.Format(timestampFormat), ks)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
}
}
result := make([]Item, 0) result := make([]Item, 0)
defer rows.Close() defer rows.Close()

View File

@ -11,5 +11,5 @@ var (
type Syncer interface { type Syncer interface {
Update(item Item) error Update(item Item) error
Updated(t time.Time) ([]Item, error) Updated(kind []Kind, t time.Time) ([]Item, error)
} }