From 5ddeb5bb5ab66182ac1baad1bd78c1f8fb4a410a Mon Sep 17 00:00:00 2001 From: Erik Winter Date: Wed, 11 Sep 2024 07:50:27 +0200 Subject: [PATCH] client --- cal/client.go | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++ cal/go.mod | 25 +++++++++++++++ cal/go.sum | 37 +++++++++++++++++++++ cal/main.go | 39 ++++++++++++++++++++++ cal/planner.go | 35 ++++++++++++++++++++ sync/go.mod | 2 +- sync/sqlite.go | 22 ++++++++----- 7 files changed, 238 insertions(+), 9 deletions(-) create mode 100644 cal/client.go create mode 100644 cal/go.mod create mode 100644 cal/go.sum create mode 100644 cal/main.go create mode 100644 cal/planner.go diff --git a/cal/client.go b/cal/client.go new file mode 100644 index 0000000..46e2b53 --- /dev/null +++ b/cal/client.go @@ -0,0 +1,87 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" +) + +type Client struct { + baseURL string + apiKey string + c *http.Client +} + +func NewClient(url, apiKey string) *Client { + return &Client{ + baseURL: url, + apiKey: apiKey, + c: &http.Client{ + Timeout: 10 * time.Second, + }, + } +} + +func (c *Client) Update(items []Item) error { + body, err := json.Marshal(items) + if err != nil { + return fmt.Errorf("could not marhal body: %v", err) + } + req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/sync", c.baseURL), bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("could not create request: %v", err) + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey)) + + res, err := c.c.Do(req) + if err != nil { + return fmt.Errorf("could not make request: %v", err) + } + if res.StatusCode != http.StatusNoContent { + return fmt.Errorf("server returned status %d", res.StatusCode) + } + + return nil +} + +func (c *Client) Updated(ks []Kind, ts time.Time) ([]Item, error) { + ksStr := make([]string, 0, len(ks)) + for _, k := range ks { + ksStr = append(ksStr, string(k)) + } + u := fmt.Sprintf("%s/sync?ks=%s", c.baseURL, strings.Join(ksStr, ",")) + if !ts.IsZero() { + u = fmt.Sprintf("%s&ts=", url.QueryEscape(ts.Format(time.RFC3339))) + } + req, err := http.NewRequest(http.MethodGet, u, nil) + if err != nil { + return nil, fmt.Errorf("could not create request: %v", err) + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey)) + + res, err := c.c.Do(req) + if err != nil { + return nil, fmt.Errorf("could not get response: %v", err) + } + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("server returned status %d", res.StatusCode) + } + + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("could not read response body: %v", err) + } + + var items []Item + if err := json.Unmarshal(body, &items); err != nil { + return nil, fmt.Errorf("could not unmarshal response body: %v", err) + } + + return items, nil +} diff --git a/cal/go.mod b/cal/go.mod new file mode 100644 index 0000000..2625d68 --- /dev/null +++ b/cal/go.mod @@ -0,0 +1,25 @@ +module code.ewintr.nl/planner/cal + +go 1.21.5 + +require ( + code.ewintr.nl/planner/sync v0.0.0-20240912051930-423effa1b447 + github.com/google/uuid v1.6.0 +) + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/sys v0.22.0 // indirect + modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/sqlite v1.33.0 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect +) + +replace code.ewintr.nl/planner/sync => ../sync diff --git a/cal/go.sum b/cal/go.sum new file mode 100644 index 0000000..26299b6 --- /dev/null +++ b/cal/go.sum @@ -0,0 +1,37 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= +golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/sqlite v1.33.0 h1:WWkA/T2G17okiLGgKAj4/RMIvgyMT19yQ038160IeYk= +modernc.org/sqlite v1.33.0/go.mod h1:9uQ9hF/pCZoYZK73D/ud5Z7cIRIILSZI8NdIemVMTX8= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/cal/main.go b/cal/main.go new file mode 100644 index 0000000..a8ace19 --- /dev/null +++ b/cal/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "os" + "time" +) + +func main() { + fmt.Println("cal") + + c := NewClient("http://localhost:8092", "testKey") + items, err := c.Updated([]Kind{KindEvent}, time.Time{}) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + fmt.Printf("%+v\n", items) + + i := Item{ + ID: "id-1", + Kind: KindEvent, + Updated: time.Now(), + Body: "body", + } + if err := c.Update([]Item{i}); err != nil { + fmt.Println(err) + os.Exit(1) + } + + items, err = c.Updated([]Kind{KindEvent}, time.Time{}) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + fmt.Printf("%+v\n", items) +} diff --git a/cal/planner.go b/cal/planner.go new file mode 100644 index 0000000..903562a --- /dev/null +++ b/cal/planner.go @@ -0,0 +1,35 @@ +package main + +import ( + "time" + + "github.com/google/uuid" +) + +type Kind string + +const ( + KindTask Kind = "task" + KindEvent Kind = "event" +) + +var ( + KnownKinds = []Kind{KindTask, KindEvent} +) + +type Item struct { + ID string `json:"id"` + Kind Kind `json:"kind"` + Updated time.Time `json:"updated"` + Deleted bool `json:"deleted"` + Body string `json:"body"` +} + +func NewItem(k Kind, body string) Item { + return Item{ + ID: uuid.New().String(), + Kind: k, + Updated: time.Now(), + Body: body, + } +} diff --git a/sync/go.mod b/sync/go.mod index e48dd81..eb45cd4 100644 --- a/sync/go.mod +++ b/sync/go.mod @@ -1,4 +1,4 @@ -module code.ewintr.nl/planner +module code.ewintr.nl/planner/sync go 1.21.5 diff --git a/sync/sqlite.go b/sync/sqlite.go index 8efb7ee..50ecc38 100644 --- a/sync/sqlite.go +++ b/sync/sqlite.go @@ -4,6 +4,7 @@ import ( "database/sql" "errors" "fmt" + "strings" "time" _ "modernc.org/sqlite" @@ -56,12 +57,11 @@ VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET -kind=?, -updated=?, -deleted=?, -body=?`, - item.ID, item.Kind, item.Updated.Format(timestampFormat), item.Deleted, item.Body, - item.Kind, item.Updated.Format(timestampFormat), item.Deleted, item.Body); err != nil { +kind=excluded.kind, +updated=excluded.updated, +deleted=excluded.deleted, +body=excluded.body`, + item.ID, item.Kind, item.Updated.Format(timestampFormat), item.Deleted, item.Body); err != nil { return fmt.Errorf("%w: %v", ErrSqliteFailure, err) } return nil @@ -80,8 +80,14 @@ WHERE updated > ?` 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) + args := []any{t.Format(timestampFormat)} + ph := make([]string, 0, len(ks)) + for _, k := range ks { + args = append(args, string(k)) + ph = append(ph, "?") + } + query = fmt.Sprintf("%s AND kind in (%s)", query, strings.Join(ph, ",")) + rows, err = s.db.Query(query, args...) if err != nil { return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err) }