feat: implement X-Total-Count header #42
@ -57,6 +57,7 @@ func main() {
|
||||
// 3.3. CORS
|
||||
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||
AllowOrigins: []string{"*"},
|
||||
ExposeHeaders: []string{"X-Total-Count"},
|
||||
}))
|
||||
|
||||
// 3.4. register implemented handlers
|
||||
|
||||
@ -55,6 +55,11 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Transactions"
|
||||
headers:
|
||||
X-Total-Count:
|
||||
description: Total number of transactions
|
||||
schema:
|
||||
type: integer
|
||||
"204":
|
||||
description: No transactions
|
||||
post:
|
||||
|
||||
@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.rosemyrtle.work/personal-finance/server/internal/dal"
|
||||
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
||||
@ -59,7 +60,7 @@ func (server *ServerImpl) GetTransactions(ctx echo.Context, params GetTransactio
|
||||
offset = int(*params.Offset)
|
||||
}
|
||||
|
||||
transactions, err := server.Dal.Transactions(limit, offset, params.Category)
|
||||
transactions, totalCount, err := server.Dal.Transactions(limit, offset, params.Category)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -68,6 +69,7 @@ func (server *ServerImpl) GetTransactions(ctx echo.Context, params GetTransactio
|
||||
return ctx.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
ctx.Response().Header().Set("X-Total-Count", strconv.FormatUint(uint64(totalCount), 10))
|
||||
return ctx.JSON(http.StatusOK, convertTransactions(transactions))
|
||||
}
|
||||
|
||||
|
||||
@ -177,7 +177,7 @@ func TestServerImpl_GetTransactions(t *testing.T) {
|
||||
|
||||
mock_dal.EXPECT().
|
||||
Transactions(int(*tt.args.params.Limit), int(*tt.args.params.Offset), tt.args.params.Category).
|
||||
Return(tt.mocks, nil).
|
||||
Return(tt.mocks, uint(1), nil).
|
||||
Times(1)
|
||||
|
||||
if err := pf.GetTransactions(ctx, tt.args.params); (err != nil) != tt.wantErr {
|
||||
|
||||
@ -276,24 +276,25 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||
var swaggerSpec = []string{
|
||||
|
||||
"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",
|
||||
"TwAAAP//vC0lzlgSAAA=",
|
||||
}
|
||||
|
||||
// GetSwagger returns the content of the embedded swagger specification file
|
||||
|
||||
@ -4,7 +4,7 @@ import "git.rosemyrtle.work/personal-finance/server/internal/entity"
|
||||
|
||||
type DAL interface {
|
||||
Transaction(transactionId int64) (*entity.Transaction, error)
|
||||
Transactions(limit, offset int, category *string) (entity.Transactions, error)
|
||||
Transactions(limit, offset int, category *string) (entity.Transactions, uint, error)
|
||||
InsertTransaction(entity.Transaction) (entity.Transaction, error)
|
||||
UpdateTransaction(entity.TransactionId, *entity.CategoryName) (bool, error)
|
||||
TransactionExists(uint64) (bool, error)
|
||||
|
||||
@ -6,8 +6,10 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type DalImpl struct {
|
||||
@ -42,7 +44,7 @@ func (dal *DalImpl) Transaction(transactionId int64) (*entity.Transaction, error
|
||||
return &transactions[0], nil
|
||||
}
|
||||
|
||||
func (dal *DalImpl) Transactions(limit, offset int, category *string) (entity.Transactions, error) {
|
||||
func (dal *DalImpl) Transactions(limit, offset int, category *string) (entity.Transactions, uint, error) {
|
||||
log.Print("DAL::Transactions", "limit", limit, "offset", offset, "category", category)
|
||||
|
||||
if dal.Db == nil {
|
||||
@ -50,7 +52,7 @@ func (dal *DalImpl) Transactions(limit, offset int, category *string) (entity.Tr
|
||||
}
|
||||
|
||||
stmts := []string{
|
||||
"SELECT t.id, t.date, t.description, t.amount, tc.name",
|
||||
"SELECT t.id, t.date, t.description, t.amount, tc.name, count(*) OVER() AS total_count",
|
||||
"FROM pfbudget.transactions t",
|
||||
"LEFT JOIN pfbudget.transactions_categorized tc ON t.id = tc.id",
|
||||
}
|
||||
@ -71,10 +73,29 @@ func (dal *DalImpl) Transactions(limit, offset int, category *string) (entity.Tr
|
||||
|
||||
rows, err := dal.Db.Query(stmt, args...)
|
||||
if err != nil {
|
||||
return entity.Transactions{}, err
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return convert[entity.Transaction](rows), nil
|
||||
type TransactionWithCount struct {
|
||||
Id entity.TransactionId
|
||||
Date time.Time
|
||||
Description string
|
||||
Value decimal.Decimal
|
||||
Category *entity.CategoryName
|
||||
TotalCount uint
|
||||
}
|
||||
|
||||
transactions_with_count := convert[TransactionWithCount](rows)
|
||||
if len(transactions_with_count) == 0 {
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
var ans entity.Transactions
|
||||
for _, twc := range transactions_with_count {
|
||||
t := entity.Transaction{Id: twc.Id, Date: twc.Date, Description: twc.Description, Value: twc.Value, Category: twc.Category}
|
||||
ans = append(ans, t)
|
||||
}
|
||||
return ans, transactions_with_count[0].TotalCount, nil
|
||||
}
|
||||
|
||||
func (dal *DalImpl) InsertTransaction(t entity.Transaction) (entity.Transaction, error) {
|
||||
|
||||
@ -116,8 +116,8 @@ func TestDalImpl_Transactions(t *testing.T) {
|
||||
fields{db},
|
||||
args{limit: 30, offset: 0, category: nil},
|
||||
[][]driver.Value{
|
||||
{1, date, "income", 1000, nil},
|
||||
{2, date, "expense", -10.50, nil},
|
||||
{1, date, "income", 1000, nil, 2},
|
||||
{2, date, "expense", -10.50, nil, 2},
|
||||
},
|
||||
entity.Transactions{
|
||||
{Id: 1, Date: date, Description: "income", Value: decimal.NewFromInt(1000)},
|
||||
@ -130,8 +130,8 @@ func TestDalImpl_Transactions(t *testing.T) {
|
||||
fields{db},
|
||||
args{limit: 30, offset: 0, category: golang.Ptr("C1")},
|
||||
[][]driver.Value{
|
||||
{1, date, "income", 1000, "C1"},
|
||||
{2, date, "expense", -10.50, nil},
|
||||
{1, date, "income", 1000, "C1", 2},
|
||||
{2, date, "expense", -10.50, nil, 2},
|
||||
},
|
||||
entity.Transactions{
|
||||
{Id: 1, Date: date, Description: "income", Value: decimal.NewFromInt(1000), Category: golang.Ptr("C1")},
|
||||
@ -151,7 +151,7 @@ func TestDalImpl_Transactions(t *testing.T) {
|
||||
|
||||
args := []driver.Value{tt.args.limit, tt.args.offset}
|
||||
stmt := `
|
||||
^SELECT \w+\.id, \w+\.date, \w+\.description, \w+\.amount, \w+\.name
|
||||
^SELECT \w+\.id, \w+\.date, \w+\.description, \w+\.amount, \w+\.name, count\(\*\) OVER\(\) AS total_count
|
||||
FROM \w+\.transactions \w+
|
||||
LEFT JOIN \w+\.transactions_categorized \w+
|
||||
ON \w+\.id = \w+\.id
|
||||
@ -170,10 +170,10 @@ func TestDalImpl_Transactions(t *testing.T) {
|
||||
ExpectQuery(stmt).
|
||||
WithArgs(args...).
|
||||
WillReturnRows(
|
||||
mock.NewRows([]string{"id", "date", "description", "amount", "category"}).AddRows(tt.mocks...),
|
||||
mock.NewRows([]string{"id", "date", "description", "amount", "category", "total_count"}).AddRows(tt.mocks...),
|
||||
)
|
||||
|
||||
got, err := dal.Transactions(tt.args.limit, tt.args.offset, tt.args.category)
|
||||
got, _, err := dal.Transactions(tt.args.limit, tt.args.offset, tt.args.category)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("DalImpl.Transactions() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
||||
@ -56,10 +56,10 @@ func (mr *MockDALMockRecorder) Bank(bankId any) *gomock.Call {
|
||||
}
|
||||
|
||||
// Banks mocks base method.
|
||||
func (m *MockDAL) Banks() ([]entity.Bank, error) {
|
||||
func (m *MockDAL) Banks() (entity.Banks, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Banks")
|
||||
ret0, _ := ret[0].([]entity.Bank)
|
||||
ret0, _ := ret[0].(entity.Banks)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
@ -71,10 +71,10 @@ func (mr *MockDALMockRecorder) Banks() *gomock.Call {
|
||||
}
|
||||
|
||||
// Categories mocks base method.
|
||||
func (m *MockDAL) Categories() ([]entity.Category, error) {
|
||||
func (m *MockDAL) Categories() (entity.Categories, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Categories")
|
||||
ret0, _ := ret[0].([]entity.Category)
|
||||
ret0, _ := ret[0].(entity.Categories)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
@ -131,12 +131,13 @@ func (mr *MockDALMockRecorder) TransactionExists(arg0 any) *gomock.Call {
|
||||
}
|
||||
|
||||
// Transactions mocks base method.
|
||||
func (m *MockDAL) Transactions(limit, offset int, category *string) ([]entity.Transaction, error) {
|
||||
func (m *MockDAL) Transactions(limit, offset int, category *string) (entity.Transactions, uint, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Transactions", limit, offset, category)
|
||||
ret0, _ := ret[0].([]entity.Transaction)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
ret0, _ := ret[0].(entity.Transactions)
|
||||
ret1, _ := ret[1].(uint)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// Transactions indicates an expected call of Transactions.
|
||||
@ -146,7 +147,7 @@ func (mr *MockDALMockRecorder) Transactions(limit, offset, category any) *gomock
|
||||
}
|
||||
|
||||
// UpdateTransaction mocks base method.
|
||||
func (m *MockDAL) UpdateTransaction(arg0 uint64, arg1 *string) (bool, error) {
|
||||
func (m *MockDAL) UpdateTransaction(arg0 entity.TransactionId, arg1 *entity.CategoryName) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateTransaction", arg0, arg1)
|
||||
ret0, _ := ret[0].(bool)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user