add kind
This commit is contained in:
parent
4f21fd8aa8
commit
5c95750a0a
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,12 @@ import (
|
||||||
type Kind string
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -67,13 +67,24 @@ 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 > ?`
|
||||||
if err != nil {
|
var rows *sql.Rows
|
||||||
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
var err error
|
||||||
|
if len(ks) == 0 {
|
||||||
|
rows, err = s.db.Query(query, t.Format(timestampFormat))
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue