Implement pagination on /transactions
All checks were successful
Go / build (1.21) (pull_request) Successful in 13s
Go / build (1.22) (pull_request) Successful in 12s
Go / build (1.21) (push) Successful in 11s
Go / build (1.22) (push) Successful in 10s

using limit and offset.
While not the best, it's the simpler to implement.

Issue: #11
This commit is contained in:
Luís Murta 2025-03-27 21:05:26 +00:00
parent 708a8d0588
commit 5552b0622a
Signed by: satprog
GPG Key ID: 169EF1BBD7049F94
8 changed files with 58 additions and 38 deletions

View File

@ -36,7 +36,8 @@ paths:
schema:
type: integer
format: int32
default: -1
default: 0
minimum: 0
- name: bank
in: query
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 {
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 {
return ctx.NoContent(http.StatusInternalServerError)
return err
}
if len(transactions) == 0 {

View File

@ -142,14 +142,14 @@ func TestServerImpl_GetTransactions(t *testing.T) {
{
"200",
fields{mock_dal},
args{GetTransactionsParams{}},
args{GetTransactionsParams{Limit: golang.Ptr[int32](100), Offset: golang.Ptr[int32](0)}},
false,
entity.Transactions{{Id: 1, Date: time.Now(), Description: "desc#1", Value: decimal.New(0, 0)}},
},
{
"204",
fields{mock_dal},
args{GetTransactionsParams{}},
args{GetTransactionsParams{Limit: golang.Ptr[int32](100), Offset: golang.Ptr[int32](0)}},
false,
entity.Transactions{},
},
@ -164,7 +164,7 @@ func TestServerImpl_GetTransactions(t *testing.T) {
rec := httptest.NewRecorder()
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 {
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
var swaggerSpec = []string{
"H4sIAAAAAAAC/8RXTXPbNhD9Kxi0R5qkI08PPLW2Jx7NdFxPk5zSHCBySSElAQZYSNV49N87ACgZ/Irk",
"OnYvNiksdt++XbwFH2kum1YKEKhp9kh1voaGucdrJv62/1slW1DIwf3KC/sXdy3QjGpUXFR0H1HBGphe",
"H4sIAAAAAAAC/8RXTXPbNhD9Kxi0R4akLU8PPLW2Jx7NdFxPk5zSHCBySSElAQZYSNV49N87ACgZ/Irk",
"OnYvNiksdt++XTwsH2kum1YKEKhp9kh1voaGucdrJv62/1slW1DIwf3KC/sXdy3QjGpUXFR0H1HBGphe",
"kKrgFYil21dK1TCkGTWGFzQamu8jquCb4QoKmn2mzsR5/nI0lauvkKP1bPF5SAiNe/hZQUkz+lPylFPS",
"JZS4bPZHN0wptrPvNwyhkmo3zrRS0rSn3B623znjgIhzNt1b22HSs/n2Q43gzlTgud7v5wr5UTGhWY5c",
"inHwPGDx/MwjWjCEXl+4H6Jx9AJ0rnh7iD5a5/324gJ/uaIRbbjgjWlolh59coFQgbKbNqw2/fBlLRk+",
"xRemWVnTAYkdyBDTwdkUtQFz5/drSPeobS0gLkrpqJcCWY72ERrGa5sFF0zk8GtjFLK4gM2IQfoASkvB",
"avLe25IPoDag/rKZ1DwHoeGpqejd/SdyBwIUq8mDWdU8J797I7JZxCmRitQMQdGIGmURrBFbnSXJdruN",
"K2Fiqaqkc6uTqq0vFnF6oZGJgtVSQLzGpnZpcqxhCt4F+aMF8dvDkizi1LINSvtM0vgyTu1e2YJgLacZ",
"XcRpvKARbRmuHdHJ6qAVFTimbO8yS4WVJXoH6MXEllm30qK0Vu/S9MAwCLePtW3Nc7cz+ap9L/qKnaM/",
"2leuX4kPJs9B69LU5IjKpvMuvbI++8b3kqw6RxHVpmmYPXP0T0DFYQME/uEauagCK5978mj/LYv9KRKu",
"d8vCUadYAwhK0+zzEMXylsjShSAoiQI0yvYNt2uW84NqZ9QHpeHxQWUgCkgb6tWXVy7Cc2pw5WMPshcb",
"VvOCLG+JNhYKFN52ol42IBESSSmNKAZVe89F4Vlc7cjy1lerU9LDDJqp1M2T1QvpOkuMjkNyUole1s9B",
"whNNHfb0wDLBga7OkdXT3xOtXfIaQdmCBN7Jcbx1Tf7NgHvpujxYPq8PhxeAIQo/d+whC3McH7YBjpo3",
"HHsgCiiZqZFml2ka9cbj4t2J8ThGJctSA5JSyYZs16CgA+SlB/ssT+HzDqYBXlxO4DuNyUsRrsEdpJm4",
"3dK85kTjNoC6IHa7bYXtmudrm6yWCmdidEv/j671OvwHnEns+/v+qOkbR7SVeuIc3ihgCOGtxg8F0Hgt",
"i91rUGGZ2I9Yv3ytUEPSP4YC4rIvZmdKaGvHxdG+x73nkDAiYBvSPpbD5DF4OzH1g9jnD/9QHU/cAXpI",
"vnsVGF3fR+f/jQ7Rm94RhrWfvSqEnHc3hoi2ZqKqn9pidNieX1TTdh86b1HU/6YEP+IrdD/6ZJuSjROF",
"81wVRB+7pN7Ndse9xLVTziPHL+kNX2zCxKQku2z+DQAA//9F7YqS5hEAAA==",
"K2Fiqaqkc6uTqq3fLeL0nUYmClZLAfEam9qlybGGKXjvyB8tiN8elmQRp5ZtUNpnksYXcWr3yhYEaznN",
"6CJO4wWNaMtw7YhOVgetqMAxZXuXWSqsLNE7QC8mtsy6lRaltbpM0wPDINw+1rY1z93O5Kv2vegrdo7+",
"aF+5fiU+mDwHrUtTkyMqm85lemV99o3vJVl1jiKqTdMwe+bon4CKwwYI/MM1clEFVj735NH+Wxb7UyRc",
"75aFo06xBhCUptnnIYrlLZGlC0FQEgVolO0bbtcs5wfVzqgPSsPjg8pAFJA21Ksvr1yE59TgysceZC82",
"rOYFWd4SbSwUKLztRL1sQCIkklIaUQyq9p6LwrO42pHlra9Wp6SHO2imUjdPVi+k6ywxOl6Sk0r0sn4O",
"Ep5o6rCnB5YJDnR1jqye/p5o7ZLXCMoWJPBOjtdb1+TfDLiXrsuD5fP6cDgADFH4e8cesjDH8WEb4Kh5",
"w7EHooCSmRppdpGmUe96XFyeuB7HqGRZakBSKtmQ7RoUdIC89GCf5Sl83sE0wJfD86qEa3BnagZCtzQv",
"P9G4I6AuiN1uu2K75vna5q2lwpkY3dL/I3G9Zv8BxxP7/r5/6/SNI9pKPXEkbxQwhHDA8fcDaLyWxe41",
"qLBM7EesX7xWqCHpH0MtcdkXs9dLaGtvjqN9j3vPIWFEwDakfayMyWPwdmIACGKfPweEQnliHOgh+e5U",
"MJrkR+f/jQ7Rm44Lw9rPTg0h593wENHWTFT1U1uMDtvzi2ra7pvnLYr635TgR3yQ7kdfb1OycaJwnquC",
"6GOX1LvZ7riXuHbKeeT4Jb3hi02YmJRkl82/AQAA///XEvLj8REAAA==",
}
// 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 {
Transaction(transactionId int64) (*entity.Transaction, error)
Transactions() (entity.Transactions, error)
Transactions(limit, offset int) (entity.Transactions, error)
InsertTransaction(entity.Transaction) (entity.Transaction, error)
UpdateTransaction(entity.TransactionId, *entity.CategoryName) (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
}
func (dal *DalImpl) Transactions() (entity.Transactions, error) {
log.Print("DAL::Transactions")
func (dal *DalImpl) Transactions(limit, offset int) (entity.Transactions, error) {
log.Print("DAL::Transactions", "limit", limit, "offset", offset)
if dal.Db == nil {
log.Panic("database not available")
@ -53,9 +53,12 @@ func (dal *DalImpl) Transactions() (entity.Transactions, error) {
FROM pfbudget.transactions t
LEFT JOIN pfbudget.transactions_categorized tc
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 {
return entity.Transactions{}, err
}

View File

@ -142,18 +142,23 @@ func TestDalImpl_Transactions(t *testing.T) {
Db: tt.fields.Db,
}
limit, offset := 0, 0
mock.
ExpectQuery(`
^SELECT \w+\.id, \w+\.date, \w+\.description, \w+\.amount, \w+\.name
FROM \w+\.transactions \w+
LEFT JOIN \w+\.transactions_categorized \w+
ON \w+\.id = \w+\.id`).
WithoutArgs().
ON \w+\.id = \w+\.id
ORDER BY \w+\.date DESC
LIMIT \$1
OFFSET \$2`).
WithArgs(limit, offset).
WillReturnRows(
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 {
t.Errorf("DalImpl.Transactions() error = %v, wantErr %v", err, tt.wantErr)
return

View File

@ -20,6 +20,7 @@ import (
type MockDAL struct {
ctrl *gomock.Controller
recorder *MockDALMockRecorder
isgomock struct{}
}
// MockDALMockRecorder is the mock recorder for MockDAL.
@ -40,18 +41,18 @@ func (m *MockDAL) EXPECT() *MockDALMockRecorder {
}
// 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()
ret := m.ctrl.Call(m, "Bank", arg0)
ret := m.ctrl.Call(m, "Bank", bankId)
ret0, _ := ret[0].(*entity.Bank)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// 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()
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.
@ -100,18 +101,18 @@ func (mr *MockDALMockRecorder) InsertTransaction(arg0 any) *gomock.Call {
}
// 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()
ret := m.ctrl.Call(m, "Transaction", arg0)
ret := m.ctrl.Call(m, "Transaction", transactionId)
ret0, _ := ret[0].(*entity.Transaction)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// 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()
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.
@ -130,18 +131,18 @@ func (mr *MockDALMockRecorder) TransactionExists(arg0 any) *gomock.Call {
}
// 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()
ret := m.ctrl.Call(m, "Transactions")
ret := m.ctrl.Call(m, "Transactions", limit, offset)
ret0, _ := ret[0].([]entity.Transaction)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// 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()
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.