From 1aa6e28b693872694ab7fbcc96e7c2cef224e0d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Murta?= Date: Sat, 9 Mar 2024 11:04:07 +0000 Subject: [PATCH] Implement basic /transactions method Issue #1 --- go.mod | 1 + go.sum | 3 ++ internal/api/converter.go | 34 ++++++++++++++++++++ internal/api/impl.go | 16 ++++++++-- internal/api/impl_test.go | 65 ++++++++++++++++++++++++++++++++++++++ internal/api/server.gen.go | 19 ++++++++++- 6 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 internal/api/converter.go create mode 100644 internal/api/impl_test.go diff --git a/go.mod b/go.mod index 1b43872..1678f1b 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module git.rosemyrtle.work/personal-finance/server go 1.21.1 require ( + github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/deepmap/oapi-codegen/v2 v2.1.0 // indirect github.com/getkin/kin-openapi v0.123.0 // indirect diff --git a/go.sum b/go.sum index 93975bb..e56ab7f 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= @@ -18,6 +20,7 @@ github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdi github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= diff --git a/internal/api/converter.go b/internal/api/converter.go new file mode 100644 index 0000000..09c916c --- /dev/null +++ b/internal/api/converter.go @@ -0,0 +1,34 @@ +package api + +import ( + "database/sql" + "log" + "reflect" +) + +func convert[T any](rows *sql.Rows) []T { + var ans []T + for rows.Next() { + var r T + s := reflect.ValueOf(&r).Elem() + log.Println(s) + + numCols := s.NumField() + columns := make([]interface{}, numCols) + log.Println(columns) + + for i := 0; i < numCols; i++ { + field := s.Field(i) + log.Println(field) + columns[i] = field.Addr().Interface() + } + log.Println(columns) + + if err := rows.Scan(columns...); err != nil { + log.Fatal(err) + } + + ans = append(ans, r) + } + return ans +} diff --git a/internal/api/impl.go b/internal/api/impl.go index 8a6b90a..57d1b44 100644 --- a/internal/api/impl.go +++ b/internal/api/impl.go @@ -1,13 +1,18 @@ package api //go:generate go run github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen --config=api.cfg.yaml ../../docs/openapi.yaml + import ( + "database/sql" + "log" "net/http" "github.com/labstack/echo/v4" ) -type ServerImpl struct{} +type ServerImpl struct { + db *sql.DB +} func (*ServerImpl) GetBanks(ctx echo.Context) error { return echo.NewHTTPError(http.StatusNotImplemented) @@ -17,8 +22,13 @@ func (*ServerImpl) GetBanksById(ctx echo.Context, bankId int64) error { return echo.NewHTTPError(http.StatusNotImplemented) } -func (*ServerImpl) GetTransactions(ctx echo.Context, params GetTransactionsParams) error { - return echo.NewHTTPError(http.StatusNotImplemented) +func (pf *ServerImpl) GetTransactions(ctx echo.Context, params GetTransactionsParams) error { + rows, err := pf.db.Query("SELECT t.category, t.date, t.description, t.id, tc.amount FROM pfbudget.transactions t LEFT JOIN pfbudget.transactions_categorized tc ON t.id = tc.id") + if err != nil { + log.Fatal(err) + } + + return ctx.JSON(http.StatusOK, convert[Transaction](rows)) } func (*ServerImpl) GetTransactionsById(ctx echo.Context, transactionId int64) error { diff --git a/internal/api/impl_test.go b/internal/api/impl_test.go new file mode 100644 index 0000000..d766d73 --- /dev/null +++ b/internal/api/impl_test.go @@ -0,0 +1,65 @@ +package api + +import ( + "log" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/labstack/echo/v4" +) + +func TestGetTransactions(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatal(err) + } + + e := echo.New() + handlers := ServerImpl{db} + RegisterHandlers(e, &handlers) + + t.Run("when successful", func(t *testing.T) { + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/transactions", nil) + + date := time.Now() + rows := mock.NewRows([]string{"category", "date", "description", "id", "amount"}). + AddRow(nil, date, "#1", 1, 1000). + AddRow("expense", date, "#2", 2, -1000) + mock.ExpectQuery("SELECT t.category, t.date, t.description, t.id, tc.amount FROM pfbudget.transactions t LEFT JOIN pfbudget.transactions_categorized tc ON t.id = tc.id").WillReturnRows(rows) + + ctx := e.NewContext(req, rec) + err := handlers.GetTransactions(ctx, GetTransactionsParams{}) + if err != nil { + t.Error(err) + } + if rec.Code != http.StatusOK { + t.Error(rec.Code) + } + + expected := `[ + { + "date":"` + date.Format(time.DateOnly) + `", + "description": "#1", + "id":1, + "value":1000 + }, + { + "category": "expense", + "date":"` + date.Format(time.DateOnly) + `", + "description": "#2", + "id":2, + "value":-1000 + } + ]` + expected = strings.Join(strings.Fields(expected), "") + log.Println(expected) + if ret := strings.TrimRight(rec.Body.String(), "\n"); ret != expected { + t.Error(ret, expected) + } + }) +} diff --git a/internal/api/server.gen.go b/internal/api/server.gen.go index 2c07977..3e29d3f 100644 --- a/internal/api/server.gen.go +++ b/internal/api/server.gen.go @@ -7,11 +7,13 @@ import ( "bytes" "compress/gzip" "encoding/base64" + "errors" "fmt" "net/http" "net/url" "path" "strings" + "time" "github.com/getkin/kin-openapi/openapi3" "github.com/labstack/echo/v4" @@ -29,10 +31,25 @@ type Bank struct { // Banks defines model for Banks. type Banks = []Bank +// Manually added date time that implements the Scanner interface +type ScannableDate struct { + openapi_types.Date +} + +func (d *ScannableDate) Scan(src any) error { + switch s := src.(type) { + case time.Time: + d.Time = s + return nil + } + + return errors.New("column is not convertible to date") +} + // Transaction defines model for Transaction. type Transaction struct { Category *string `json:"category,omitempty"` - Date openapi_types.Date `json:"date"` + Date ScannableDate `json:"date"` Description string `json:"description"` Id int64 `json:"id"` Value float32 `json:"value"`