Implement pagination on /transactions #36

Merged
satprog merged 1 commits from feature/pagination into main 2025-03-27 21:29:29 +00:00
8 changed files with 58 additions and 38 deletions
Showing only changes of commit 5552b0622a - Show all commits

View File

@ -36,7 +36,8 @@ paths:
schema: schema:
type: integer type: integer
format: int32 format: int32
default: -1 default: 0
minimum: 0
- name: bank - name: bank
in: query in: query
description: ID of the bank description: ID of the bank

View File

@ -49,9 +49,19 @@ func (server *ServerImpl) GetBankById(ctx echo.Context, bankId string) error {
func (server *ServerImpl) GetTransactions(ctx echo.Context, params GetTransactionsParams) error { func (server *ServerImpl) GetTransactions(ctx echo.Context, params GetTransactionsParams) error {
log.Print("GetTransactions") log.Print("GetTransactions")
transactions, err := server.Dal.Transactions() limit := 100
if params.Limit != nil {
limit = int(*params.Limit)
}
offset := 0
if params.Offset != nil {
offset = int(*params.Offset)
}
transactions, err := server.Dal.Transactions(limit, offset)
if err != nil { if err != nil {
return ctx.NoContent(http.StatusInternalServerError) return err
} }
if len(transactions) == 0 { if len(transactions) == 0 {

View File

@ -142,14 +142,14 @@ func TestServerImpl_GetTransactions(t *testing.T) {
{ {
"200", "200",
fields{mock_dal}, fields{mock_dal},
args{GetTransactionsParams{}}, args{GetTransactionsParams{Limit: golang.Ptr[int32](100), Offset: golang.Ptr[int32](0)}},
false, false,
entity.Transactions{{Id: 1, Date: time.Now(), Description: "desc#1", Value: decimal.New(0, 0)}}, entity.Transactions{{Id: 1, Date: time.Now(), Description: "desc#1", Value: decimal.New(0, 0)}},
}, },
{ {
"204", "204",
fields{mock_dal}, fields{mock_dal},
args{GetTransactionsParams{}}, args{GetTransactionsParams{Limit: golang.Ptr[int32](100), Offset: golang.Ptr[int32](0)}},
false, false,
entity.Transactions{}, entity.Transactions{},
}, },
@ -164,7 +164,7 @@ func TestServerImpl_GetTransactions(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
ctx := echo.New().NewContext(req, rec) ctx := echo.New().NewContext(req, rec)
mock_dal.EXPECT().Transactions().Return(tt.mocks, nil).Times(1) mock_dal.EXPECT().Transactions(int(*tt.args.params.Limit), int(*tt.args.params.Offset)).Return(tt.mocks, nil).Times(1)
if err := pf.GetTransactions(ctx, tt.args.params); (err != nil) != tt.wantErr { if err := pf.GetTransactions(ctx, tt.args.params); (err != nil) != tt.wantErr {
t.Errorf("ServerImpl.GetTransactions() error = %v,", err) t.Errorf("ServerImpl.GetTransactions() error = %v,", err)

View File

@ -276,24 +276,24 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
// Base64 encoded, gzipped, json marshaled Swagger object // Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{ var swaggerSpec = []string{
"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",
} }
// GetSwagger returns the content of the embedded swagger specification file // GetSwagger returns the content of the embedded swagger specification file

View File

@ -4,7 +4,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(limit, offset int) (entity.Transactions, error)
InsertTransaction(entity.Transaction) (entity.Transaction, error) InsertTransaction(entity.Transaction) (entity.Transaction, error)
UpdateTransaction(entity.TransactionId, *entity.CategoryName) (bool, error) UpdateTransaction(entity.TransactionId, *entity.CategoryName) (bool, error)
TransactionExists(uint64) (bool, error) TransactionExists(uint64) (bool, error)

View File

@ -41,8 +41,8 @@ func (dal *DalImpl) Transaction(transactionId int64) (*entity.Transaction, error
return &transactions[0], nil return &transactions[0], nil
} }
func (dal *DalImpl) Transactions() (entity.Transactions, error) { func (dal *DalImpl) Transactions(limit, offset int) (entity.Transactions, error) {
log.Print("DAL::Transactions") log.Print("DAL::Transactions", "limit", limit, "offset", offset)
if dal.Db == nil { if dal.Db == nil {
log.Panic("database not available") log.Panic("database not available")
@ -53,9 +53,12 @@ func (dal *DalImpl) Transactions() (entity.Transactions, error) {
FROM pfbudget.transactions t FROM pfbudget.transactions t
LEFT JOIN pfbudget.transactions_categorized tc LEFT JOIN pfbudget.transactions_categorized tc
ON t.id = tc.id ON t.id = tc.id
ORDER BY t.date DESC
LIMIT $1
OFFSET $2
` `
rows, err := dal.Db.Query(stmt) rows, err := dal.Db.Query(stmt, limit, offset)
if err != nil { if err != nil {
return entity.Transactions{}, err return entity.Transactions{}, err
} }

View File

@ -142,18 +142,23 @@ func TestDalImpl_Transactions(t *testing.T) {
Db: tt.fields.Db, Db: tt.fields.Db,
} }
limit, offset := 0, 0
mock. mock.
ExpectQuery(` ExpectQuery(`
^SELECT \w+\.id, \w+\.date, \w+\.description, \w+\.amount, \w+\.name ^SELECT \w+\.id, \w+\.date, \w+\.description, \w+\.amount, \w+\.name
FROM \w+\.transactions \w+ FROM \w+\.transactions \w+
LEFT JOIN \w+\.transactions_categorized \w+ LEFT JOIN \w+\.transactions_categorized \w+
ON \w+\.id = \w+\.id`). ON \w+\.id = \w+\.id
WithoutArgs(). ORDER BY \w+\.date DESC
LIMIT \$1
OFFSET \$2`).
WithArgs(limit, offset).
WillReturnRows( WillReturnRows(
mock.NewRows([]string{"id", "date", "description", "amount", "category"}).AddRows(tt.mocks...), mock.NewRows([]string{"id", "date", "description", "amount", "category"}).AddRows(tt.mocks...),
) )
got, err := dal.Transactions() got, err := dal.Transactions(limit, offset)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("DalImpl.Transactions() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("DalImpl.Transactions() error = %v, wantErr %v", err, tt.wantErr)
return return

View File

@ -20,6 +20,7 @@ import (
type MockDAL struct { type MockDAL struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockDALMockRecorder recorder *MockDALMockRecorder
isgomock struct{}
} }
// MockDALMockRecorder is the mock recorder for MockDAL. // MockDALMockRecorder is the mock recorder for MockDAL.
@ -40,18 +41,18 @@ func (m *MockDAL) EXPECT() *MockDALMockRecorder {
} }
// Bank mocks base method. // Bank mocks base method.
func (m *MockDAL) Bank(arg0 string) (*entity.Bank, error) { func (m *MockDAL) Bank(bankId string) (*entity.Bank, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Bank", arg0) ret := m.ctrl.Call(m, "Bank", bankId)
ret0, _ := ret[0].(*entity.Bank) ret0, _ := ret[0].(*entity.Bank)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// Bank indicates an expected call of Bank. // Bank indicates an expected call of Bank.
func (mr *MockDALMockRecorder) Bank(arg0 any) *gomock.Call { func (mr *MockDALMockRecorder) Bank(bankId any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bank", reflect.TypeOf((*MockDAL)(nil).Bank), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bank", reflect.TypeOf((*MockDAL)(nil).Bank), bankId)
} }
// Banks mocks base method. // Banks mocks base method.
@ -100,18 +101,18 @@ func (mr *MockDALMockRecorder) InsertTransaction(arg0 any) *gomock.Call {
} }
// Transaction mocks base method. // Transaction mocks base method.
func (m *MockDAL) Transaction(arg0 int64) (*entity.Transaction, error) { func (m *MockDAL) Transaction(transactionId int64) (*entity.Transaction, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Transaction", arg0) ret := m.ctrl.Call(m, "Transaction", transactionId)
ret0, _ := ret[0].(*entity.Transaction) ret0, _ := ret[0].(*entity.Transaction)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// Transaction indicates an expected call of Transaction. // Transaction indicates an expected call of Transaction.
func (mr *MockDALMockRecorder) Transaction(arg0 any) *gomock.Call { func (mr *MockDALMockRecorder) Transaction(transactionId any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Transaction", reflect.TypeOf((*MockDAL)(nil).Transaction), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Transaction", reflect.TypeOf((*MockDAL)(nil).Transaction), transactionId)
} }
// TransactionExists mocks base method. // TransactionExists mocks base method.
@ -130,18 +131,18 @@ func (mr *MockDALMockRecorder) TransactionExists(arg0 any) *gomock.Call {
} }
// Transactions mocks base method. // Transactions mocks base method.
func (m *MockDAL) Transactions() ([]entity.Transaction, error) { func (m *MockDAL) Transactions(limit, offset int) ([]entity.Transaction, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Transactions") ret := m.ctrl.Call(m, "Transactions", limit, offset)
ret0, _ := ret[0].([]entity.Transaction) ret0, _ := ret[0].([]entity.Transaction)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// Transactions indicates an expected call of Transactions. // Transactions indicates an expected call of Transactions.
func (mr *MockDALMockRecorder) Transactions() *gomock.Call { func (mr *MockDALMockRecorder) Transactions(limit, offset any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Transactions", reflect.TypeOf((*MockDAL)(nil).Transactions)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Transactions", reflect.TypeOf((*MockDAL)(nil).Transactions), limit, offset)
} }
// UpdateTransaction mocks base method. // UpdateTransaction mocks base method.