diff --git a/app/main.go b/app/main.go index 19cb287..043e78c 100644 --- a/app/main.go +++ b/app/main.go @@ -72,7 +72,7 @@ func doList(db *gorm.DB) { Alive: &TRUE, } - certs, err := repo.List(filter) + certs, err := repo.List(filter, nil, nil) if err != nil { panic(err) } diff --git a/app/repository/method_list.go b/app/repository/method_list.go index 3fc52e2..b39d948 100644 --- a/app/repository/method_list.go +++ b/app/repository/method_list.go @@ -1,12 +1,30 @@ package repository import ( + "fmt" "repo-pattern/app/repository/smartfilter" "gorm.io/gorm" "gorm.io/gorm/schema" ) +type Pagination struct { + Offset int + Limit int +} + +type OrderDirection string + +const ( + OrderASC OrderDirection = "ASC" + OrderDESC OrderDirection = "DESC" +) + +type Order struct { + Field string + Direction OrderDirection +} + type ListMethod[T schema.Tabler] struct { DbConn *gorm.DB } @@ -15,7 +33,36 @@ func (m *ListMethod[T]) Init(dbConn *gorm.DB) { m.DbConn = dbConn } -func (m ListMethod[T]) List(filter interface{}) (*[]T, error) { +func applyOrdering(query *gorm.DB, ordering *[]Order) *gorm.DB { + if ordering == nil || len(*ordering) == 0 { + return query + } + + for _, order := range *ordering { + if order.Direction == OrderASC { + query = query.Order(order.Field) + } else { + query = query.Order(fmt.Sprintf("%s %s", order.Field, order.Direction)) + } + } + return query +} + +func applyPagination(query *gorm.DB, pagination *Pagination) *gorm.DB { + if pagination == nil { + return query + } + + if pagination.Limit != 0 { + query = query.Limit(pagination.Limit) + } + if pagination.Offset != 0 { + query = query.Offset(pagination.Offset) + } + return query +} + +func (m ListMethod[T]) List(filter interface{}, ordering *[]Order, pagination *Pagination) (*[]T, error) { var ( model T models []T @@ -26,6 +73,9 @@ func (m ListMethod[T]) List(filter interface{}) (*[]T, error) { return nil, err } + query = applyOrdering(query, ordering) + query = applyPagination(query, pagination) + query.Find(&models) return &models, nil } diff --git a/app/repository/repository_test.go b/app/repository/repository_test.go index ae288d8..aadc283 100644 --- a/app/repository/repository_test.go +++ b/app/repository/repository_test.go @@ -2,6 +2,7 @@ package repository import ( "database/sql" + "fmt" "log" "regexp" "testing" @@ -46,8 +47,113 @@ type MyModelFilter struct { Count *int `filterfield:"field=count,operator=GT"` } -func TestRepository(t *testing.T) { - t.Run("List with simple filter", func(t *testing.T) { +func TestListMethod(t *testing.T) { + t.Run("With ordering", func(t *testing.T) { + sqldb, db, mock := NewMockDB() + defer sqldb.Close() + + repo := RepoBase[MyModel]{} + repo.Init(db) + + filter := MyModelFilter{} + ordering := []Order{ + { + Field: "id", + Direction: OrderASC, + }, + { + Field: "count", + Direction: OrderDESC, + }, + } + + sql := "SELECT * FROM my_models ORDER BY id,count DESC" + mock.ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(sql))) + + _, err := repo.List(filter, &ordering, nil) + assert.Nil(t, err) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + + t.Run("With limit", func(t *testing.T) { + sqldb, db, mock := NewMockDB() + defer sqldb.Close() + + repo := RepoBase[MyModel]{} + repo.Init(db) + + filter := MyModelFilter{} + pagination := Pagination{ + Limit: 111, + Offset: 0, + } + + sql := "SELECT * FROM my_models LIMIT $1" + mock.ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(sql))). + WithArgs(pagination.Limit) + + _, err := repo.List(filter, nil, &pagination) + assert.Nil(t, err) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + + t.Run("With offset", func(t *testing.T) { + sqldb, db, mock := NewMockDB() + defer sqldb.Close() + + repo := RepoBase[MyModel]{} + repo.Init(db) + + filter := MyModelFilter{} + pagination := Pagination{ + Limit: 0, + Offset: 222, + } + + sql := "SELECT * FROM my_models OFFSET $1" + mock.ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(sql))). + WithArgs(pagination.Offset) + + _, err := repo.List(filter, nil, &pagination) + assert.Nil(t, err) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + + t.Run("With limit and offset", func(t *testing.T) { + sqldb, db, mock := NewMockDB() + defer sqldb.Close() + + repo := RepoBase[MyModel]{} + repo.Init(db) + + filter := MyModelFilter{} + pagination := Pagination{ + Limit: 111, + Offset: 222, + } + + sql := "SELECT * FROM my_models LIMIT $1 OFFSET $2" + mock.ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(sql))). + WithArgs(pagination.Limit, pagination.Offset) + + _, err := repo.List(filter, nil, &pagination) + assert.Nil(t, err) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + + t.Run("Simple filter", func(t *testing.T) { sqldb, db, mock := NewMockDB() defer sqldb.Close() @@ -60,9 +166,10 @@ func TestRepository(t *testing.T) { } sql := "SELECT * FROM my_models WHERE my_models.id = $1" - mock.ExpectQuery(regexp.QuoteMeta(sql)).WithArgs(id) + mock.ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(sql))). + WithArgs(id) - _, err := repo.List(filter) + _, err := repo.List(filter, nil, nil) assert.Nil(t, err) if err := mock.ExpectationsWereMet(); err != nil { @@ -70,7 +177,7 @@ func TestRepository(t *testing.T) { } }) - t.Run("List with multiple filter values", func(t *testing.T) { + t.Run("Multiple filter values", func(t *testing.T) { sqldb, db, mock := NewMockDB() defer sqldb.Close() @@ -87,9 +194,42 @@ func TestRepository(t *testing.T) { } sql := "SELECT * FROM my_models WHERE my_models.id = $1 AND my_models.value = $2 AND my_models.count > $3" - mock.ExpectQuery(regexp.QuoteMeta(sql)).WithArgs(id, value, count) + mock.ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(sql))). + WithArgs(id, value, count) - _, err := repo.List(filter) + _, err := repo.List(filter, nil, nil) + assert.Nil(t, err) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + + t.Run("Multiple filter values and pagination", func(t *testing.T) { + sqldb, db, mock := NewMockDB() + defer sqldb.Close() + + repo := RepoBase[MyModel]{} + repo.Init(db) + + id := 123 + count := 456 + value := "some value" + filter := MyModelFilter{ + Id: &id, + Value: &value, + Count: &count, + } + pagination := Pagination{ + Offset: 111, + Limit: 222, + } + + sql := "SELECT * FROM my_models WHERE my_models.id = $1 AND my_models.value = $2 AND my_models.count > $3 LIMIT $4 OFFSET $5" + mock.ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(sql))). + WithArgs(id, value, count, pagination.Limit, pagination.Offset) + + _, err := repo.List(filter, nil, &pagination) assert.Nil(t, err) if err := mock.ExpectationsWereMet(); err != nil {