diff --git a/cmd/api-server/main.go b/cmd/api-server/main.go index 8093d83..e7f4afb 100644 --- a/cmd/api-server/main.go +++ b/cmd/api-server/main.go @@ -12,6 +12,8 @@ import ( _ "github.com/jackc/pgx/v5/stdlib" "github.com/joho/godotenv" "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + oapimiddleware "github.com/oapi-codegen/echo-middleware" ) func main() { @@ -41,6 +43,14 @@ func main() { // 3. HTTP server e := echo.New() + e.Use(middleware.Logger()) + + swagger, err := api.GetSwagger() + if err != nil { + log.Fatalf("unable to get API swagger: %v\n", err) + } + e.Use(oapimiddleware.OapiRequestValidator(swagger)) + handlers := api.ServerImpl{Dal: &dal} api.RegisterHandlers(e, &handlers) diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 3f46f9c..28e7587 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -56,6 +56,23 @@ paths: $ref: "#/components/schemas/Transactions" "204": description: No transactions + post: + summary: Create a new transaction + operationId: createTransaction + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Transaction" + responses: + "201": + description: Transaction created + content: + application/json: + schema: + $ref: "#/components/schemas/Transaction" + "400": + description: Transaction not created /transaction/{transactionId}: get: @@ -138,7 +155,6 @@ components: category: type: string required: - - id - date - description - value diff --git a/go.mod b/go.mod index a7324f2..0328c0c 100644 --- a/go.mod +++ b/go.mod @@ -7,12 +7,19 @@ require ( github.com/deepmap/oapi-codegen/v2 v2.1.0 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.5.5 + github.com/oapi-codegen/echo-middleware v1.0.1 github.com/oapi-codegen/runtime v1.1.1 github.com/shopspring/decimal v1.4.0 github.com/stretchr/testify v1.9.0 go.uber.org/mock v0.4.0 ) +require ( + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/gorilla/mux v1.8.1 // indirect + golang.org/x/time v0.5.0 // indirect +) + require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index 7d8cb50..91b82ed 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,12 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 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/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -52,6 +56,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/oapi-codegen/echo-middleware v1.0.1 h1:edYGScq1phCcuDoz9AqA9eHX+tEI1LNL5PL1lkkQh1k= +github.com/oapi-codegen/echo-middleware v1.0.1/go.mod h1:DBQKRn+D/vfXOFbaX5GRwFttoJY64JH6yu+pdt7wU3o= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= @@ -92,6 +98,8 @@ golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/api/converter.go b/internal/api/converter.go index 0200565..8f98667 100644 --- a/internal/api/converter.go +++ b/internal/api/converter.go @@ -3,17 +3,41 @@ package api import ( "git.rosemyrtle.work/personal-finance/server/internal/entity" openapi_types "github.com/oapi-codegen/runtime/types" + "github.com/shopspring/decimal" ) -func convertTransaction(t entity.Transaction) Transaction { +func ptr[T any](v T) *T { + return &v +} + +func entity2transaction(t entity.Transaction) Transaction { return Transaction{ - nil, openapi_types.Date{Time: t.Date}, t.Description, int64(t.Id), float32(t.Value.InexactFloat64())} + nil, + openapi_types.Date{Time: t.Date}, + t.Description, + ptr(int64(t.Id)), + float32(t.Value.InexactFloat64()), + } +} + +func transaction2entity(t Transaction) entity.Transaction { + var id uint64 = entity.InvalidId + if t.Id != nil { + id = uint64(*t.Id) + } + + return entity.Transaction{ + Id: id, + Date: t.Date.Time, + Description: t.Description, + Value: decimal.NewFromFloat32(t.Value), + } } func convertTransactions(ts entity.Transactions) Transactions { var ans Transactions for _, t := range ts { - ans = append(ans, convertTransaction(t)) + ans = append(ans, entity2transaction(t)) } return ans } diff --git a/internal/api/impl.go b/internal/api/impl.go index 77072b0..e71cc2a 100644 --- a/internal/api/impl.go +++ b/internal/api/impl.go @@ -73,5 +73,21 @@ func (server *ServerImpl) GetTransactionById(ctx echo.Context, transactionId int return ctx.NoContent(http.StatusNotFound) } - return ctx.JSON(http.StatusOK, convertTransaction(*transaction)) + return ctx.JSON(http.StatusOK, entity2transaction(*transaction)) +} + +func (server *ServerImpl) CreateTransaction(ctx echo.Context) error { + t := new(Transaction) + if err := ctx.Bind(t); err != nil { + log.Printf("%v", err) + return ctx.NoContent(http.StatusBadRequest) + } + + ans, err := server.Dal.InsertTransaction(transaction2entity(*t)) + if err != nil { + log.Printf("%v", err) + return ctx.NoContent(http.StatusInternalServerError) + } + + return ctx.JSON(http.StatusCreated, entity2transaction(ans)) } diff --git a/internal/api/impl_test.go b/internal/api/impl_test.go index 1ac86cd..092e5cf 100644 --- a/internal/api/impl_test.go +++ b/internal/api/impl_test.go @@ -3,7 +3,9 @@ package api import ( "net/http" "net/http/httptest" + "reflect" "strconv" + "strings" "testing" "time" @@ -224,3 +226,63 @@ func TestServerImpl_GetTransactionById(t *testing.T) { }) } } + +func TestServerImpl_CreateTransaction(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mock_dal := mock.NewMockDAL(ctrl) + + date, _ := time.Parse(time.DateOnly, "1974-04-25") + + type fields struct { + Dal dal.DAL + } + type args struct { + request string + } + type want struct { + entity entity.Transaction + response string + } + tests := []struct { + name string + fields fields + args args + want want + wantErr bool + }{ + { + "201", + fields{mock_dal}, + args{`{"date": "1974-04-25", "description": "freedom", "value": 9000}`}, + want{ + entity.Transaction{Id: 1, Date: date, Description: "freedom", Value: decimal.New(9000, 0)}, + `{"date":"1974-04-25","description":"freedom","id":1,"value":9000}`, + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &ServerImpl{Dal: tt.fields.Dal} + + req := httptest.NewRequest(http.MethodPost, "/transactions", strings.NewReader(tt.args.request)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + e := echo.New() + ctx := e.NewContext(req, rec) + + inserted := tt.want.entity + inserted.Id = entity.InvalidId + mock_dal.EXPECT().InsertTransaction(gomock.AssignableToTypeOf(reflect.TypeOf(tt.want.entity))).Return(tt.want.entity, nil).Times(1) + + if err := s.CreateTransaction(ctx); (err != nil) != tt.wantErr { + t.Errorf("ServerImpl.GetTransactionById() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(rec.Body.String(), tt.want.response) { + t.Errorf("DalImpl.InsertTransaction() = %v, want %v", rec.Body.String(), tt.want.response) + } + }) + } +} diff --git a/internal/api/server.gen.go b/internal/api/server.gen.go index c3a7b30..927fd40 100644 --- a/internal/api/server.gen.go +++ b/internal/api/server.gen.go @@ -34,7 +34,7 @@ type Transaction struct { Category *string `json:"category,omitempty"` Date openapi_types.Date `json:"date"` Description string `json:"description"` - Id int64 `json:"id"` + Id *int64 `json:"id,omitempty"` Value float32 `json:"value"` } @@ -59,6 +59,9 @@ type GetTransactionsParams struct { Sort *string `form:"sort,omitempty" json:"sort,omitempty"` } +// CreateTransactionJSONRequestBody defines body for CreateTransaction for application/json ContentType. +type CreateTransactionJSONRequestBody = Transaction + // ServerInterface represents all server handlers. type ServerInterface interface { // Find bank by ID @@ -73,6 +76,9 @@ type ServerInterface interface { // Retrieve existing transactions // (GET /transactions) GetTransactions(ctx echo.Context, params GetTransactionsParams) error + // Create a new transaction + // (POST /transactions) + CreateTransaction(ctx echo.Context) error } // ServerInterfaceWrapper converts echo contexts to parameters. @@ -167,6 +173,15 @@ func (w *ServerInterfaceWrapper) GetTransactions(ctx echo.Context) error { return err } +// CreateTransaction converts echo context to params. +func (w *ServerInterfaceWrapper) CreateTransaction(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.CreateTransaction(ctx) + return err +} + // This is a simple interface which specifies echo.Route addition functions which // are present on both echo.Echo and echo.Group, since we want to allow using // either of them for path registration @@ -199,27 +214,29 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/banks", wrapper.GetBanks) router.GET(baseURL+"/transaction/:transactionId", wrapper.GetTransactionById) router.GET(baseURL+"/transactions", wrapper.GetTransactions) + router.POST(baseURL+"/transactions", wrapper.CreateTransaction) } // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "jkMmmIZdTHto9N8AAAD//+YRCdlGDQAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/dal/dal.go b/internal/dal/dal.go index 458ac55..df190bc 100644 --- a/internal/dal/dal.go +++ b/internal/dal/dal.go @@ -5,6 +5,7 @@ import "git.rosemyrtle.work/personal-finance/server/internal/entity" type DAL interface { Transaction(transactionId int64) (*entity.Transaction, error) Transactions() (entity.Transactions, error) + InsertTransaction(entity.Transaction) (entity.Transaction, error) Bank(bankId string) (*entity.Bank, error) Banks() (entity.Banks, error) } diff --git a/internal/dal/impl.go b/internal/dal/impl.go index 441a06b..418f368 100644 --- a/internal/dal/impl.go +++ b/internal/dal/impl.go @@ -46,6 +46,28 @@ func (dal *DalImpl) Transactions() (entity.Transactions, error) { return convert[entity.Transaction](rows), nil } +func (dal *DalImpl) InsertTransaction(t entity.Transaction) (entity.Transaction, error) { + log.Print("DAL::InsertTransaction") + + if dal.Db == nil { + log.Panic("database not available") + } + + stmt := ` + INSERT INTO pfbudget.transactions (date, description, value) + VALUES ($1, $2, $3) + RETURNING id + ` + + id := new(uint64) + if err := dal.Db.QueryRow(stmt, t.Date, t.Description, t.Value).Scan(id); err != nil { + return entity.Transaction{}, err + } + + t.Id = *id + return t, nil +} + func (dal *DalImpl) Bank(bankId string) (*entity.Bank, error) { log.Printf("DAL::Bank(%v)", bankId) diff --git a/internal/dal/impl_test.go b/internal/dal/impl_test.go index 0b5f55f..207c5cd 100644 --- a/internal/dal/impl_test.go +++ b/internal/dal/impl_test.go @@ -260,3 +260,36 @@ func TestDalImpl_Banks(t *testing.T) { }) } } + +func TestDalImpl_InsertTransaction(t *testing.T) { + type fields struct { + Db *sql.DB + } + type args struct { + t entity.Transaction + } + tests := []struct { + name string + fields fields + args args + want entity.Transaction + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dal := &DalImpl{ + Db: tt.fields.Db, + } + got, err := dal.InsertTransaction(tt.args.t) + if (err != nil) != tt.wantErr { + t.Errorf("DalImpl.InsertTransaction() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("DalImpl.InsertTransaction() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/entity/entity.go b/internal/entity/entity.go index c59e01b..0edc391 100644 --- a/internal/entity/entity.go +++ b/internal/entity/entity.go @@ -7,6 +7,8 @@ import ( "github.com/shopspring/decimal" ) +const InvalidId uint64 = 0 + type Transaction struct { Id uint64 Date time.Time diff --git a/internal/mock/mock_dal.gen.go b/internal/mock/mock_dal.gen.go index 220bd37..b7d78d1 100644 --- a/internal/mock/mock_dal.gen.go +++ b/internal/mock/mock_dal.gen.go @@ -69,6 +69,21 @@ func (mr *MockDALMockRecorder) Banks() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Banks", reflect.TypeOf((*MockDAL)(nil).Banks)) } +// InsertTransaction mocks base method. +func (m *MockDAL) InsertTransaction(arg0 entity.Transaction) (entity.Transaction, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertTransaction", arg0) + ret0, _ := ret[0].(entity.Transaction) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InsertTransaction indicates an expected call of InsertTransaction. +func (mr *MockDALMockRecorder) InsertTransaction(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertTransaction", reflect.TypeOf((*MockDAL)(nil).InsertTransaction), arg0) +} + // Transaction mocks base method. func (m *MockDAL) Transaction(arg0 int64) (*entity.Transaction, error) { m.ctrl.T.Helper()