From 5b90a3a1bb280eb68f9615ec8f8948cbd67bbfa4 Mon Sep 17 00:00:00 2001 From: Nitin Gupta Date: Sat, 6 Jul 2024 14:35:31 +0100 Subject: [PATCH] adding crud support --- go.mod | 3 +- go.sum | 13 +- internal/crud/crud.go | 192 ++++++++++++++++ internal/crud/crud_test.go | 435 ++++++++++++++++++++++++++++++++++++ internal/crud/pagination.go | 78 +++++++ 5 files changed, 713 insertions(+), 8 deletions(-) create mode 100644 internal/crud/crud.go create mode 100644 internal/crud/crud_test.go create mode 100644 internal/crud/pagination.go diff --git a/go.mod b/go.mod index 0d952d691..b3081d32a 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,8 @@ require ( github.com/jackc/pgpassfile v1.0.0 github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 github.com/jackc/puddle/v2 v2.2.1 - github.com/stretchr/testify v1.8.1 + github.com/pashagolub/pgxmock/v3 v3.4.0 + github.com/stretchr/testify v1.9.0 golang.org/x/crypto v0.17.0 golang.org/x/text v0.14.0 ) diff --git a/go.sum b/go.sum index 29fe452b2..51a7d5243 100644 --- a/go.sum +++ b/go.sum @@ -16,19 +16,18 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pashagolub/pgxmock/v3 v3.4.0 h1:87VMr2q7m2+6VzXo4Tsp9kMklGlj6mMN19Hp/bp2Rwo= +github.com/pashagolub/pgxmock/v3 v3.4.0/go.mod h1:FvCl7xqPbLLI3XohihJ1NzXnikjM3q/NWSixg4t9hrU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= diff --git a/internal/crud/crud.go b/internal/crud/crud.go new file mode 100644 index 000000000..173e1ae00 --- /dev/null +++ b/internal/crud/crud.go @@ -0,0 +1,192 @@ +package crud + +import ( + "context" + "errors" + "fmt" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "math" +) + +type RowMapperFunc func(pgx.Row) (interface{}, error) + +type DBPool interface { + Ping(ctx context.Context) error + Begin(ctx context.Context) (pgx.Tx, error) + Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) + QueryRow(ctx context.Context, sql string, args ...any) pgx.Row + Close() +} + +var _ DBPool = (*pgxpool.Pool)(nil) + +//go:generate mockery --name CRUD +type CRUD interface { + BeginTransaction() (pgx.Tx, error) + CommitTransaction(tx pgx.Tx) error + RollBackTransaction(tx pgx.Tx) error + Delete(query string, args ...any) error + Update(query string, args ...any) error + Create(query string, args ...any) (interface{}, error) + GetOne(query string, mapper RowMapperFunc, args ...any) (interface{}, error) + Get(query string, mapper RowMapperFunc, args ...any) ([]interface{}, error) + GetWithPagination(finalSQL string, mapper RowMapperFunc, pagination *Pagination, args ...any) (*Pagination, error) + GetCountForPagination(countSql string, args ...any) (int64, error) +} + +type crud struct { + db DBPool + lock bool +} + +func NewCrudOperation(db DBPool) CRUD { + return &crud{ + db: db, + } +} + +func (crud *crud) Delete(query string, args ...any) error { + // Begin a transaction + tx, txErr := crud.BeginTransaction() + if txErr != nil { + return txErr + } + // Execute the DELETE statement within the transaction + cmdTag, err := tx.Exec(context.Background(), query, args...) + if err != nil { + fmt.Println(fmt.Errorf("failed to delete from database: %v", err)) + return crud.RollBackTransaction(tx) + } + fmt.Printf("Rows Affected by delete:%v \n", cmdTag.RowsAffected()) + if cmdTag.RowsAffected() == 0 { + fmt.Printf("No object found with the given criteria to delete") + return errors.New("no rows affected") + } + // Commit the transaction + return crud.CommitTransaction(tx) +} +func (crud *crud) Create(query string, args ...any) (interface{}, error) { + var id interface{} + // Begin a transaction + tx, txErr := crud.BeginTransaction() + if txErr != nil { + return -1, txErr + } + if err := tx.QueryRow(context.Background(), query, args...).Scan(&id); err != nil { + fmt.Println(fmt.Errorf("failed to create object in database: %v", err)) + fmt.Println(fmt.Errorf("rolling back Create transaction")) + return -1, crud.RollBackTransaction(tx) + } + + cErr := crud.CommitTransaction(tx) + if cErr != nil { + return -1, crud.RollBackTransaction(tx) + } + return id, nil +} +func (crud *crud) BeginTransaction() (pgx.Tx, error) { + tx, err := crud.db.Begin(context.Background()) + if err != nil { + return nil, err + } + return tx, nil +} + +func (crud *crud) CommitTransaction(tx pgx.Tx) error { + if err := tx.Commit(context.Background()); err != nil { + fmt.Println(fmt.Errorf("failed to commit transaction for update: %v", err)) + return err + } + return nil +} + +func (crud *crud) RollBackTransaction(tx pgx.Tx) error { + if rollbackErr := tx.Rollback(context.Background()); rollbackErr != nil { + fmt.Println(fmt.Errorf("failed to rollback transaction for update: %v", rollbackErr)) + return rollbackErr + } + return errors.New(fmt.Sprintf("Failed to update object")) +} + +func (crud *crud) Update(query string, args ...any) error { + // Begin a transaction + tx, txerr := crud.BeginTransaction() + if txerr != nil { + return txerr + } + // Execute the UPDATE statement within the transaction + cmdTag, err := tx.Exec(context.Background(), query, args...) + if err != nil { + // If an error occurs, rollback the transaction + return crud.RollBackTransaction(tx) + } + + // Check if any row was actually updated + if cmdTag.RowsAffected() == 0 { + // No rows affected, might want to handle this as an error or just a no-op + fmt.Printf("no object found with the given criteria to update\n") + return errors.New("no rows affected") + } + return crud.CommitTransaction(tx) +} + +func (crud *crud) GetCountForPagination(countSql string, args ...any) (int64, error) { + ctx := context.Background() + var totalRows int64 + err := crud.db.QueryRow(ctx, countSql, args...).Scan(&totalRows) + if err != nil { + fmt.Println(fmt.Errorf("Failed to count total rows: %v", err)) + return 0, err + } + return totalRows, nil +} +func (crud *crud) GetWithPagination(finalSQL string, mapper RowMapperFunc, pagination *Pagination, args ...any) (*Pagination, error) { + ctx := context.Background() + rows, err := crud.db.Query(ctx, finalSQL, args...) + if err != nil { + fmt.Println(fmt.Errorf("Failed to execute query: %v", err)) + return nil, err + } + defer rows.Close() + var results []interface{} + for rows.Next() { + item, err := mapper(rows) + if err != nil { + return nil, err + } + results = append(results, item) + } + pagination.TotalPages = int64(math.Ceil(float64(pagination.TotalRows) / float64(pagination.GetLimit()))) + pagination.Rows = results + return pagination, nil +} + +func (crud *crud) Get(query string, mapper RowMapperFunc, args ...any) ([]interface{}, error) { + + ctx := context.Background() + rows, err := crud.db.Query(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var results []interface{} + for rows.Next() { + item, err := mapper(rows) + if err != nil { + return nil, err + } + results = append(results, item) + } + return results, nil +} + +func (crud *crud) GetOne(query string, mapper RowMapperFunc, args ...any) (interface{}, error) { + row := crud.db.QueryRow(context.Background(), query, args...) + item, err := mapper(row) + if err != nil { + return nil, err + } + return item, nil +} diff --git a/internal/crud/crud_test.go b/internal/crud/crud_test.go new file mode 100644 index 000000000..2cac8ce06 --- /dev/null +++ b/internal/crud/crud_test.go @@ -0,0 +1,435 @@ +package crud + +import ( + "errors" + "github.com/jackc/pgx/v5" + "github.com/pashagolub/pgxmock/v3" + "github.com/stretchr/testify/assert" + "testing" +) + +type testStruct struct { + id int +} + +var testMapper = func(row pgx.Row) (interface{}, error) { + var test testStruct + err := row.Scan(&test.id) + if err != nil { + return nil, err + } + return &test, nil +} + +func Test_Get_Success(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectQuery(`SELECT * `). + WithArgs(1). + WillReturnRows(pgxmock.NewRows([]string{"id"}).AddRow(1)) + _, err := c.Get(`SELECT * `, testMapper, 1) + assert.Nil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_Get_Count(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectQuery(`SELECT COUNT`). + WillReturnRows(pgxmock.NewRows([]string{"count"}). + AddRow(int64(1))) + count, err := c.GetCountForPagination(`SELECT COUNT(*)`) + assert.Nil(t, err) + assert.Equal(t, int64(1), count) +} +func Test_Get_Paginated_Success(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectQuery(`SELECT * `). + WithArgs(1). + WillReturnRows(pgxmock.NewRows([]string{"id"}).AddRow(1)) + + _, err := c.GetWithPagination(`SELECT *`, testMapper, &Pagination{}, 1) + assert.Nil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} +func Test_GetOne_Success(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectQuery(`SELECT * `). + WithArgs(1). + WillReturnRows(pgxmock.NewRows([]string{"id"}).AddRow(1)) + _, err := c.GetOne(`SELECT * `, testMapper, 1) + assert.Nil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_CRUDRepository_Delete(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectBegin() + dbMock.ExpectExec(`DELETE`). + WithArgs(1).WillReturnResult(pgxmock.NewResult("DELETE", 1)) + dbMock.ExpectCommit() + + err := c.Delete(`DELETE FROM "users" WHERE "userEmailId" = $1`, 1) + if err != nil { + t.Error(err) + } + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_CRUDRepository_Create(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectBegin() + dbMock.ExpectQuery(`INSERT INTO`). + WithArgs(1).WillReturnRows(pgxmock.NewRows([]string{"id"}).AddRow(1)) + dbMock.ExpectCommit() + _, err := c.Create(`INSERT INTO`, 1) + assert.Nil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_CRUDRepository_Update(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectBegin() + dbMock.ExpectExec(`Update`). + WithArgs(1).WillReturnResult(pgxmock.NewResult("Update", 1)) + dbMock.ExpectCommit() + err := c.Update(`Update`, 1) + assert.Nil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_CRUDRepository_Delete2(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectBegin() + dbMock.ExpectExec(`DELETE`). + WithArgs(1).WillReturnResult(pgxmock.NewResult("DELETE", 0)) + + c.Delete(`DELETE FROM "users" WHERE "userEmailId" = $1`, 1) + + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_CRUDRepository_Delete_Fail(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectBegin() + dbMock.ExpectExec(`DELETE`). + WithArgs(1).WillReturnError(errors.New("some error")) + dbMock.ExpectRollback() + + err := c.Delete(`DELETE FROM "users" WHERE "userEmailId" = $1`, 1) + assert.NotNil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_CRUDRepository_Delete_Fail2(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectBegin().WillReturnError(errors.New("some error")) + err := c.Delete(`DELETE FROM "users" WHERE "userEmailId" = $1`, 1) + assert.NotNil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_CRUDRepository_Delete_Fail3(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectBegin() + dbMock.ExpectExec(`DELETE`). + WithArgs(1).WillReturnResult(pgxmock.NewResult("DELETE", 1)) + dbMock.ExpectCommit().WillReturnError(errors.New("some error")) + + err := c.Delete(`DELETE FROM "users" WHERE "userEmailId" = $1`, 1) + assert.NotNil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_CRUDRepository_Delete_Fail5(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectBegin() + dbMock.ExpectExec(`DELETE`). + WithArgs(1).WillReturnError(errors.New("some error")) + dbMock.ExpectRollback().WillReturnError(errors.New("some error")) + + err := c.Delete(`DELETE FROM "users" WHERE "userEmailId" = $1`, 1) + assert.NotNil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_CRUDRepository_Create_Fail1(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectBegin().WillReturnError(errors.New("some error")) + _, err := c.Create(`INSERT INTO`, nil) + assert.NotNil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_CRUDRepository_Create_Fail2(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectBegin() + dbMock.ExpectQuery(`INSERT INTO`). + WithArgs(1). + WillReturnError(errors.New("some error")) + dbMock.ExpectRollback() + _, err := c.Create(`INSERT INTO`, 1) + assert.NotNil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_CRUDRepository_Create_Fail3(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectBegin() + dbMock.ExpectQuery(`INSERT INTO`). + WithArgs(1).WillReturnRows(pgxmock.NewRows([]string{"id"}).AddRow(1)) + dbMock.ExpectCommit().WillReturnError(errors.New("some error")) + dbMock.ExpectRollback() + _, err := c.Create(`INSERT INTO`, 1) + assert.NotNil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_CRUDRepository_Create_Fail4(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectBegin() + dbMock.ExpectQuery(`INSERT INTO`). + WithArgs(1).WillReturnRows(pgxmock.NewRows([]string{"id"}).AddRow(1)) + dbMock.ExpectCommit().WillReturnError(errors.New("some error")) + dbMock.ExpectRollback().WillReturnError(errors.New("some error")) + _, err := c.Create(`INSERT INTO`, 1) + assert.NotNil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_CRUDRepository_Update_Fail1(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectBegin().WillReturnError(errors.New("some error")) + err := c.Update(`Update`, nil) + assert.NotNil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_CRUDRepository_Update_Fail2(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectBegin() + dbMock.ExpectExec(`Update`). + WithArgs(1). + WillReturnError(errors.New("some error")) + dbMock.ExpectRollback() + err := c.Update(`Update`, 1) + assert.NotNil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_CRUDRepository_Update_Fail3(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectBegin() + dbMock.ExpectExec(`Update`).WithArgs(1).WillReturnResult(pgxmock.NewResult("Update", 0)) + err := c.Update(`Update`, 1) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } + assert.NotNil(t, err) +} + +func Test_CRUDRepository_Update_Fail4(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectBegin() + dbMock.ExpectExec(`Update`).WithArgs(1).WillReturnResult(pgxmock.NewResult("Update", 1)) + dbMock.ExpectCommit().WillReturnError(errors.New("some error")) + err := c.Update(`Update`, 1) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } + assert.NotNil(t, err) +} +func Test_CRUDRepository_Update_Fail5(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectBegin() + dbMock.ExpectExec(`Update`). + WithArgs(1). + WillReturnError(errors.New("some error")) + dbMock.ExpectRollback().WillReturnError(errors.New("some error")) + err := c.Update(`Update`, 1) + assert.NotNil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_GetOne_Fail(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectQuery(`SELECT * `). + WithArgs(1). + WillReturnRows(pgxmock.NewRows([]string{"id"})) + _, err := c.GetOne(`SELECT * `, testMapper, 1) + assert.NotNil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_Get_Paginated_Failure1(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectQuery(`SELECT * `). + WithArgs(1). + WillReturnError(errors.New("some error")) + _, err := c.GetWithPagination(`SELECT *`, testMapper, &Pagination{}, 1) + assert.NotNil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_Get_Paginated_Failure2(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectQuery(`SELECT * `). + WithArgs(1). + WillReturnRows(pgxmock.NewRows([]string{"id"}).AddRow("a")) + _, err := c.GetWithPagination(`SELECT *`, testMapper, &Pagination{}, 1) + assert.NotNil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_Get_Paginated_Failure3(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectQuery(`SELECT * `). + WithArgs(1). + WillReturnRows(pgxmock.NewRows([]string{"id"}).AddRow(1)) + _, err := c.GetWithPagination(`SELECT *`, testMapper, &Pagination{}, 1) + assert.Nil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_Get_Fail1(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectQuery(`SELECT * `). + WithArgs(1). + WillReturnRows(pgxmock.NewRows([]string{"id"}).AddRow("a")) + _, err := c.Get(`SELECT * `, testMapper, 1) + assert.NotNil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} + +func Test_Get_Fail2(t *testing.T) { + dbMock, _ := pgxmock.NewPool() + + c := NewCrudOperation(dbMock) + defer dbMock.Close() + dbMock.ExpectQuery(`SELECT * `). + WithArgs(1). + WillReturnError(errors.New("some error")) + _, err := c.Get(`SELECT * `, testMapper, 1) + assert.NotNil(t, err) + if e := dbMock.ExpectationsWereMet(); e != nil { + t.Errorf("there were unfulfilled expectations: %s", e) + } +} diff --git a/internal/crud/pagination.go b/internal/crud/pagination.go new file mode 100644 index 000000000..631ed7598 --- /dev/null +++ b/internal/crud/pagination.go @@ -0,0 +1,78 @@ +package crud + +import ( + "errors" + "fmt" +) + +// StringContains checks if a string is present in a slice of strings. +// Returns true if the string is found; otherwise, returns false. +func StringContains(slice []string, str string) bool { + for _, item := range slice { + if item == str { + return true + } + } + return false +} + +type Pagination struct { + Limit int64 `json:"limit,omitempty;query:limit"` + Page int64 `json:"page,omitempty;query:page"` + Sort string `json:"sort,omitempty;query:sort"` + TotalRows int64 `json:"total_rows"` + TotalPages int64 `json:"total_pages"` + Rows []interface{} `json:"rows"` +} + +func (p *Pagination) GetOffset() int64 { + return (p.GetPage() - 1) * p.GetLimit() +} +func (p *Pagination) GetLimit() int64 { + if p.Limit == 0 { + p.Limit = 10 + } + return p.Limit +} +func (p *Pagination) GetPage() int64 { + if p.Page == 0 { + p.Page = 1 + } + return p.Page +} +func (p *Pagination) GetSort() string { + if p.Sort == "" { + p.Sort = "\"id\" desc" + } + return p.Sort +} + +// PaginateQueryExtractor extracts pagination query parameters from a Gin context +// and returns a config.Pagination object along with an error message if any. +func PaginateQueryExtractor(page, pageSize int64, sort, direction string, validSortFields []string) (*Pagination, + error) { + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = 10 // Default to 10 if not specified or invalid + } + var sortWithDirection string + if sort != "" { + // Validate sort field + if !StringContains(validSortFields, sort) { + return nil, errors.New("Invalid sort field") + } + if direction == "true" { + sortWithDirection = fmt.Sprintf("\"%s\" DESC", sort) + } else { + sortWithDirection = fmt.Sprintf("\"%s\" ASC", sort) + } + } + + return &Pagination{ + Limit: pageSize, + Page: page, + Sort: sortWithDirection, + }, nil +}