diff --git a/sync-service/handler.go b/sync-service/handler.go index 93711f0..b7a862c 100644 --- a/sync-service/handler.go +++ b/sync-service/handler.go @@ -7,6 +7,7 @@ import ( "log/slog" "net/http" "path" + "slices" "strings" "time" ) @@ -66,8 +67,21 @@ func (s *Server) SyncGet(w http.ResponseWriter, r *http.Request) { 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 { msg := err.Error() 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) 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 == "" { msg := fmt.Sprintf(`{"error":"item %s does not have a body"}`, item.ID) http.Error(w, msg, http.StatusBadRequest) diff --git a/sync-service/handler_test.go b/sync-service/handler_test.go index a1e4fbd..6cb17a4 100644 --- a/sync-service/handler_test.go +++ b/sync-service/handler_test.go @@ -11,6 +11,7 @@ import ( "net/url" "os" "sort" + "strings" "testing" "time" ) @@ -56,9 +57,9 @@ func TestSyncGet(t *testing.T) { mem := NewMemory() items := []Item{ - {ID: "id-0", Updated: now.Add(-10 * time.Minute)}, - {ID: "id-1", Updated: now.Add(-5 * time.Minute)}, - {ID: "id-2", Updated: now.Add(time.Minute)}, + {ID: "id-0", Kind: KindEvent, Updated: now.Add(-10 * time.Minute)}, + {ID: "id-1", Kind: KindEvent, Updated: now.Add(-5 * time.Minute)}, + {ID: "id-2", Kind: KindTask, Updated: now.Add(time.Minute)}, } for _, item := range items { @@ -73,6 +74,7 @@ func TestSyncGet(t *testing.T) { for _, tc := range []struct { name string ts time.Time + ks []string expStatus int expItems []Item }{ @@ -82,14 +84,28 @@ func TestSyncGet(t *testing.T) { expItems: items, }, { - name: "normal", + name: "new", ts: now.Add(-6 * time.Minute), expStatus: http.StatusOK, 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) { 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) if err != nil { t.Errorf("exp nil, got %v", err) @@ -101,6 +117,10 @@ func TestSyncGet(t *testing.T) { if res.Result().StatusCode != tc.expStatus { t.Errorf("exp %v, got %v", tc.expStatus, res.Result().StatusCode) } + if tc.expStatus != http.StatusOK { + return + } + var actItems []Item actBody, err := io.ReadAll(res.Result().Body) if err != nil { @@ -149,20 +169,20 @@ func TestSyncPost(t *testing.T) { { name: "invalid item", 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, }, { name: "normal", reqBody: []byte(`[ - {"id":"id-1","kind":"test","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-1","kind":"event","updated":"2024-09-06T08:00:00Z","deleted":false,"body":"item"}, + {"id":"id-2","kind":"event","updated":"2024-09-06T08:12:00Z","deleted":false,"body":"item2"} ]`), expStatus: http.StatusNoContent, expItems: []Item{ - {ID: "id-1", 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-1", Kind: KindEvent, Updated: time.Date(2024, 9, 6, 8, 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) } - actItems, err := mem.Updated(time.Time{}) + actItems, err := mem.Updated([]Kind{}, time.Time{}) if err != nil { t.Errorf("exp nil, git %v", err) } diff --git a/sync-service/memory.go b/sync-service/memory.go index 276fe1c..b6ae65c 100644 --- a/sync-service/memory.go +++ b/sync-service/memory.go @@ -1,6 +1,7 @@ package main import ( + "slices" "time" ) @@ -20,11 +21,13 @@ func (m *Memory) Update(item Item) error { 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) 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) } } diff --git a/sync-service/memory_test.go b/sync-service/memory_test.go index 8cc615e..717161d 100644 --- a/sync-service/memory_test.go +++ b/sync-service/memory_test.go @@ -11,7 +11,7 @@ func TestMemoryItem(t *testing.T) { mem := NewMemory() t.Log("start empty") - actItems, actErr := mem.Updated(time.Time{}) + actItems, actErr := mem.Updated([]Kind{}, time.Time{}) if actErr != nil { t.Errorf("exp nil, got %v", actErr) } @@ -20,11 +20,11 @@ func TestMemoryItem(t *testing.T) { } t.Log("add one") - t1 := NewItem("test") + t1 := NewItem(Kind("kinda"), "test") if actErr := mem.Update(t1); actErr != nil { t.Errorf("exp nil, got %v", actErr) } - actItems, actErr = mem.Updated(time.Time{}) + actItems, actErr = mem.Updated([]Kind{}, time.Time{}) if actErr != nil { t.Errorf("exp nil, got %v", actErr) } @@ -38,11 +38,11 @@ func TestMemoryItem(t *testing.T) { before := time.Now() t.Log("add second") - t2 := NewItem("test 2") + t2 := NewItem(Kind("kindb"), "test 2") if actErr := mem.Update(t2); actErr != nil { t.Errorf("exp nil, got %v", actErr) } - actItems, actErr = mem.Updated(time.Time{}) + actItems, actErr = mem.Updated([]Kind{}, time.Time{}) if actErr != nil { 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) } - actItems, actErr = mem.Updated(before) + actItems, actErr = mem.Updated([]Kind{}, before) if actErr != nil { t.Errorf("exp nil, got %v", actErr) } @@ -72,7 +72,7 @@ func TestMemoryItem(t *testing.T) { if actErr := mem.Update(t1); actErr != nil { t.Errorf("exp nil, got %v", actErr) } - actItems, actErr = mem.Updated(before) + actItems, actErr = mem.Updated([]Kind{}, before) if actErr != nil { t.Errorf("exp nil, got %v", actErr) } @@ -85,4 +85,16 @@ func TestMemoryItem(t *testing.T) { if 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) + } } diff --git a/sync-service/planner.go b/sync-service/planner.go index 6bfba7e..903562a 100644 --- a/sync-service/planner.go +++ b/sync-service/planner.go @@ -9,7 +9,12 @@ import ( type Kind string const ( - KindTask Kind = "task" + KindTask Kind = "task" + KindEvent Kind = "event" +) + +var ( + KnownKinds = []Kind{KindTask, KindEvent} ) type Item struct { @@ -20,32 +25,11 @@ type Item struct { Body string `json:"body"` } -func NewItem(body string) Item { +func NewItem(k Kind, body string) Item { return Item{ ID: uuid.New().String(), + Kind: k, Updated: time.Now(), 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 -} diff --git a/sync-service/sqlite.go b/sync-service/sqlite.go index 12b445e..8efb7ee 100644 --- a/sync-service/sqlite.go +++ b/sync-service/sqlite.go @@ -67,13 +67,24 @@ body=?`, return nil } -func (s *Sqlite) Updated(t time.Time) ([]Item, error) { - rows, err := s.db.Query(` +func (s *Sqlite) Updated(ks []Kind, t time.Time) ([]Item, error) { + query := ` SELECT id, kind, updated, deleted, body FROM items -WHERE updated > ?`, t.Format(timestampFormat)) - if err != nil { - return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err) +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 { + 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) diff --git a/sync-service/storage.go b/sync-service/storage.go index 779e606..62152ad 100644 --- a/sync-service/storage.go +++ b/sync-service/storage.go @@ -11,5 +11,5 @@ var ( type Syncer interface { Update(item Item) error - Updated(t time.Time) ([]Item, error) + Updated(kind []Kind, t time.Time) ([]Item, error) }