Implements /transactions POST method
It adds the method to the OpenAPI spec and generates a new server config. The requirement for the ID on the Transaction component is removed, so that it can be reused for insertions. It also adds two new middlewares, a logging and a spec validator. If a request does not follow the spec, a 400 is returned immediately. Issue: #18
This commit is contained in:
parent
59f4546a81
commit
a6a350c901
@ -12,6 +12,8 @@ import (
|
|||||||
_ "github.com/jackc/pgx/v5/stdlib"
|
_ "github.com/jackc/pgx/v5/stdlib"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/labstack/echo/v4/middleware"
|
||||||
|
echomiddleware "github.com/oapi-codegen/echo-middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -41,6 +43,14 @@ func main() {
|
|||||||
|
|
||||||
// 3. HTTP server
|
// 3. HTTP server
|
||||||
e := echo.New()
|
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(echomiddleware.OapiRequestValidator(swagger))
|
||||||
|
|
||||||
handlers := api.ServerImpl{Dal: &dal}
|
handlers := api.ServerImpl{Dal: &dal}
|
||||||
api.RegisterHandlers(e, &handlers)
|
api.RegisterHandlers(e, &handlers)
|
||||||
|
|
||||||
|
|||||||
@ -56,6 +56,23 @@ paths:
|
|||||||
$ref: "#/components/schemas/Transactions"
|
$ref: "#/components/schemas/Transactions"
|
||||||
"204":
|
"204":
|
||||||
description: No transactions
|
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}:
|
/transaction/{transactionId}:
|
||||||
get:
|
get:
|
||||||
@ -138,7 +155,6 @@ components:
|
|||||||
category:
|
category:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- id
|
|
||||||
- date
|
- date
|
||||||
- description
|
- description
|
||||||
- value
|
- value
|
||||||
|
|||||||
8
go.mod
8
go.mod
@ -7,6 +7,9 @@ require (
|
|||||||
github.com/deepmap/oapi-codegen/v2 v2.1.0
|
github.com/deepmap/oapi-codegen/v2 v2.1.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jackc/pgx/v5 v5.5.5
|
github.com/jackc/pgx/v5 v5.5.5
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/labstack/echo/v4 v4.12.0
|
||||||
|
github.com/oapi-codegen/echo-middleware v1.0.1
|
||||||
github.com/oapi-codegen/runtime v1.1.1
|
github.com/oapi-codegen/runtime v1.1.1
|
||||||
github.com/shopspring/decimal v1.4.0
|
github.com/shopspring/decimal v1.4.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
@ -19,13 +22,13 @@ require (
|
|||||||
github.com/getkin/kin-openapi v0.124.0
|
github.com/getkin/kin-openapi v0.124.0
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
github.com/go-openapi/swag v0.23.0 // indirect
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
github.com/invopop/yaml v0.3.1 // indirect
|
github.com/invopop/yaml v0.3.1 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
github.com/joho/godotenv v1.5.1
|
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/labstack/echo/v4 v4.12.0
|
|
||||||
github.com/labstack/gommon v0.4.2 // indirect
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
@ -41,6 +44,7 @@ require (
|
|||||||
golang.org/x/sync v0.7.0 // indirect
|
golang.org/x/sync v0.7.0 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
golang.org/x/text v0.15.0 // indirect
|
golang.org/x/text v0.15.0 // indirect
|
||||||
|
golang.org/x/time v0.5.0 // indirect
|
||||||
golang.org/x/tools v0.21.0 // indirect
|
golang.org/x/tools v0.21.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
8
go.sum
8
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-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 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
|
||||||
github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
|
github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
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/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 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
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 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
|
||||||
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
|
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=
|
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/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 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
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 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
|
||||||
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
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=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@ -3,17 +3,41 @@ package api
|
|||||||
import (
|
import (
|
||||||
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
||||||
openapi_types "github.com/oapi-codegen/runtime/types"
|
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{
|
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 {
|
func convertTransactions(ts entity.Transactions) Transactions {
|
||||||
var ans Transactions
|
var ans Transactions
|
||||||
for _, t := range ts {
|
for _, t := range ts {
|
||||||
ans = append(ans, convertTransaction(t))
|
ans = append(ans, entity2transaction(t))
|
||||||
}
|
}
|
||||||
return ans
|
return ans
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,5 +73,21 @@ func (server *ServerImpl) GetTransactionById(ctx echo.Context, transactionId int
|
|||||||
return ctx.NoContent(http.StatusNotFound)
|
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))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,9 @@ package api
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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(strings.TrimSpace(rec.Body.String()), tt.want.response) {
|
||||||
|
t.Errorf("DalImpl.InsertTransaction() = %v, want %v", rec.Body.String(), tt.want.response)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -34,7 +34,7 @@ type Transaction struct {
|
|||||||
Category *string `json:"category,omitempty"`
|
Category *string `json:"category,omitempty"`
|
||||||
Date openapi_types.Date `json:"date"`
|
Date openapi_types.Date `json:"date"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Id int64 `json:"id"`
|
Id *int64 `json:"id,omitempty"`
|
||||||
Value float32 `json:"value"`
|
Value float32 `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,6 +59,9 @@ type GetTransactionsParams struct {
|
|||||||
Sort *string `form:"sort,omitempty" json:"sort,omitempty"`
|
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.
|
// ServerInterface represents all server handlers.
|
||||||
type ServerInterface interface {
|
type ServerInterface interface {
|
||||||
// Find bank by ID
|
// Find bank by ID
|
||||||
@ -73,6 +76,9 @@ type ServerInterface interface {
|
|||||||
// Retrieve existing transactions
|
// Retrieve existing transactions
|
||||||
// (GET /transactions)
|
// (GET /transactions)
|
||||||
GetTransactions(ctx echo.Context, params GetTransactionsParams) error
|
GetTransactions(ctx echo.Context, params GetTransactionsParams) error
|
||||||
|
// Create a new transaction
|
||||||
|
// (POST /transactions)
|
||||||
|
CreateTransaction(ctx echo.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerInterfaceWrapper converts echo contexts to parameters.
|
// ServerInterfaceWrapper converts echo contexts to parameters.
|
||||||
@ -167,6 +173,15 @@ func (w *ServerInterfaceWrapper) GetTransactions(ctx echo.Context) error {
|
|||||||
return err
|
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
|
// 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
|
// are present on both echo.Echo and echo.Group, since we want to allow using
|
||||||
// either of them for path registration
|
// 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+"/banks", wrapper.GetBanks)
|
||||||
router.GET(baseURL+"/transaction/:transactionId", wrapper.GetTransactionById)
|
router.GET(baseURL+"/transaction/:transactionId", wrapper.GetTransactionById)
|
||||||
router.GET(baseURL+"/transactions", wrapper.GetTransactions)
|
router.GET(baseURL+"/transactions", wrapper.GetTransactions)
|
||||||
|
router.POST(baseURL+"/transactions", wrapper.CreateTransaction)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||||
var swaggerSpec = []string{
|
var swaggerSpec = []string{
|
||||||
|
|
||||||
"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",
|
||||||
|
"jkMmmIZdTHto9N8AAAD//+YRCdlGDQAA",
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSwagger returns the content of the embedded swagger specification file
|
// GetSwagger returns the content of the embedded swagger specification file
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import "git.rosemyrtle.work/personal-finance/server/internal/entity"
|
|||||||
type DAL interface {
|
type DAL interface {
|
||||||
Transaction(transactionId int64) (*entity.Transaction, error)
|
Transaction(transactionId int64) (*entity.Transaction, error)
|
||||||
Transactions() (entity.Transactions, error)
|
Transactions() (entity.Transactions, error)
|
||||||
|
InsertTransaction(entity.Transaction) (entity.Transaction, error)
|
||||||
Bank(bankId string) (*entity.Bank, error)
|
Bank(bankId string) (*entity.Bank, error)
|
||||||
Banks() (entity.Banks, error)
|
Banks() (entity.Banks, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,6 +46,28 @@ func (dal *DalImpl) Transactions() (entity.Transactions, error) {
|
|||||||
return convert[entity.Transaction](rows), nil
|
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) {
|
func (dal *DalImpl) Bank(bankId string) (*entity.Bank, error) {
|
||||||
log.Printf("DAL::Bank(%v)", bankId)
|
log.Printf("DAL::Bank(%v)", bankId)
|
||||||
|
|
||||||
|
|||||||
@ -260,3 +260,60 @@ func TestDalImpl_Banks(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDalImpl_InsertTransaction(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
date := time.Now()
|
||||||
|
|
||||||
|
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
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"201",
|
||||||
|
fields{db},
|
||||||
|
args{entity.Transaction{Id: entity.InvalidId, Date: date, Description: "freedom", Value: decimal.NewFromInt(9000)}},
|
||||||
|
entity.Transaction{Id: 1, Date: date, Description: "freedom", Value: decimal.NewFromInt(9000)},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
dal := &DalImpl{
|
||||||
|
Db: tt.fields.Db,
|
||||||
|
}
|
||||||
|
|
||||||
|
mock.
|
||||||
|
ExpectQuery(`
|
||||||
|
INSERT INTO .* \(date, description, value\)
|
||||||
|
VALUES \(\$1, \$2, \$3\)
|
||||||
|
RETURNING id`).
|
||||||
|
WithArgs(tt.args.t.Date, tt.args.t.Description, tt.args.t.Value).
|
||||||
|
WillReturnRows(
|
||||||
|
mock.NewRows([]string{"id"}).
|
||||||
|
AddRows([]driver.Value{tt.want.Id}),
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import (
|
|||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const InvalidId uint64 = 0
|
||||||
|
|
||||||
type Transaction struct {
|
type Transaction struct {
|
||||||
Id uint64
|
Id uint64
|
||||||
Date time.Time
|
Date time.Time
|
||||||
|
|||||||
@ -69,6 +69,21 @@ func (mr *MockDALMockRecorder) Banks() *gomock.Call {
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Banks", reflect.TypeOf((*MockDAL)(nil).Banks))
|
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.
|
// Transaction mocks base method.
|
||||||
func (m *MockDAL) Transaction(arg0 int64) (*entity.Transaction, error) {
|
func (m *MockDAL) Transaction(arg0 int64) (*entity.Transaction, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user