Compare commits

..

3 Commits

Author SHA1 Message Date
295e915f89 Pagination and ordering 2024-06-24 00:45:54 +02:00
642ac4fba7 Repo tests 2024-06-23 23:33:53 +02:00
39757cde34 Cleanup 2024-06-23 18:44:50 +02:00
13 changed files with 390 additions and 435 deletions

View File

@ -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)
}
@ -122,7 +122,6 @@ func main() {
defer logging.Log.Sync()
db := db.InitDB()
repository.Dao = repository.CreateDAO(db)
doMagic(db)
// doList(db)

View File

@ -1,23 +0,0 @@
package models
import (
"time"
"github.com/google/uuid"
)
type ApiKey struct {
Key uuid.UUID `gorm:"type(uuid);unique;default:uuid_generate_v4()"`
CompanyId uuid.UUID `gorm:"type(uuid)" faker:"-"`
ActiveFrom *time.Time `faker:"-"`
ActiveTo *time.Time `faker:"-"`
LastUsed *time.Time
CreatedAt time.Time `faker:"-"`
UpdatedAt time.Time `faker:"-"`
Company Company `faker:"-"`
}
func (m *ApiKey) TableName() string {
return "api_keys"
}

View File

@ -1,27 +0,0 @@
package models
import (
"time"
"github.com/google/uuid"
"gorm.io/datatypes"
)
type FiskLogItem struct {
Id uuid.UUID `gorm:"type(uuid);unique"`
ApiKeyId uuid.UUID `gorm:"type(uuid)"`
CompanyId uuid.UUID `gorm:"type(uuid)"`
IsDemo bool
RequestData datatypes.JSONMap `gorm:"type:jsonb;not null"`
ResponseData *datatypes.JSONMap `gorm:"type:jsonb"`
IznosUkupno int
Zki *string
Jir *uuid.UUID `gorm:"type(uuid)"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (m *FiskLogItem) TableName() string {
return "fisk_log"
}

View File

@ -1,89 +0,0 @@
package repository
import (
"repo-pattern/app/lib/helpers"
"repo-pattern/app/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
type ApiKeysRepository struct {
db *gorm.DB
}
type ApiKeysFilter struct {
Key *uuid.UUID
CompanyId *uuid.UUID
IsActive *bool
CompanyIsActive *bool
}
func CreateApiKeysRepository(db *gorm.DB) *ApiKeysRepository {
return &ApiKeysRepository{db}
}
func applyApiKeyFilter(db *gorm.DB, query *gorm.DB, filter *ApiKeysFilter) *gorm.DB {
if filter.Key != nil {
query.Where("key = ?", *filter.Key)
}
if filter.IsActive != nil {
now := helpers.UTCNow()
if *filter.IsActive {
query.Where("active_from IS NULL OR active_from <= ?", now)
query.Where("active_to IS NULL OR active_to >= ?", now)
} else {
query.Where(
"active_from IS NOT NULL AND active_from >= ?", now,
).Or(
"active_to IS NOT NULL AND active_to =< ?", now,
)
}
}
if filter.CompanyId != nil {
query.Where("company_id = ?", *filter.CompanyId)
}
if filter.CompanyIsActive != nil {
query.Joins("Company").Where(
map[string]interface{}{"Company.is_active": *filter.CompanyIsActive},
)
}
return query
}
func (r *ApiKeysRepository) New(companyId uuid.UUID) *models.ApiKey {
now := helpers.UTCNow()
return &models.ApiKey{
CompanyId: companyId,
ActiveFrom: &now,
CreatedAt: now,
UpdatedAt: now,
}
}
func (r *ApiKeysRepository) Get(filter *ApiKeysFilter) *models.ApiKey {
var api_key models.ApiKey
query := r.db.Model(&models.ApiKey{})
applyApiKeyFilter(r.db, query, filter)
result := query.First(&api_key).Joins("Company")
if result.Error != nil {
return nil
}
return &api_key
}
func (r *ApiKeysRepository) Save(model *models.ApiKey) {
model.UpdatedAt = helpers.UTCNow()
// we don't have standard model Id, but Key as primary key.
// this could be possible reason for save not assigning generated key uuid
// to model.Key. but this works.
if model.Key == uuid.Nil {
r.db.Create(model)
} else {
r.db.Save(model)
}
}

View File

@ -1,101 +0,0 @@
package repository
import (
"errors"
"repo-pattern/app/lib/helpers"
"repo-pattern/app/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
type CertRepository struct {
db *gorm.DB
}
type CertFilter struct {
Alive *bool
Id *uuid.UUID
CompanyId *uuid.UUID
CompanyIsActive *bool
}
func CreateCertRepository(db *gorm.DB) *CertRepository {
return &CertRepository{db}
}
func applyCertFilter(db *gorm.DB, query *gorm.DB, filter *CertFilter) *gorm.DB {
if filter.Id != nil {
query.Where("certificates.id = ?", *filter.Id)
}
if filter.Alive == nil {
query.Where("certificates.alive = TRUE")
} else {
query.Where("certificates.alive = ?", *filter.Alive)
}
if filter.CompanyId != nil {
query.Where("certificates.company_id = ?", *filter.CompanyId)
}
if filter.CompanyIsActive != nil {
query.Joins("LEFT JOIN companies on companies.is_active = TRUE")
// query.Joins("Company").Where(
// map[string]interface{}{"Company.is_active": *filter.CompanyIsActive},
// )
}
return query
}
func (r *CertRepository) New(companyId uuid.UUID) *models.Cert {
now := helpers.UTCNow()
return &models.Cert{
Alive: true,
CompanyId: companyId,
CreatedAt: now,
UpdatedAt: now,
}
}
func (r *CertRepository) Get(filter *CertFilter) *models.Cert {
var cert models.Cert
query := r.db.Model(&models.Cert{})
applyCertFilter(r.db, query, filter)
result := query.First(&cert).Joins("Company")
if result.Error != nil {
return nil
}
return &cert
}
func (r *CertRepository) GetForCompany(companyId uuid.UUID) *models.Cert {
isAlive := true
filter := CertFilter{
Alive: &isAlive,
CompanyId: &companyId,
}
return r.Get(&filter)
}
func (r *CertRepository) Save(model *models.Cert) {
model.UpdatedAt = helpers.UTCNow()
r.db.Save(model)
}
func (r *CertRepository) Exists(filter *CertFilter) bool {
var row models.Cert
query := r.db.Model(&models.Cert{})
query = applyCertFilter(r.db, query, filter)
result := query.Select("certificates.id").First(&row)
return !errors.Is(result.Error, gorm.ErrRecordNotFound) && result.Error == nil
}
func (r *CertRepository) Delete(filter *CertFilter) {
query := r.db.Model(&models.Cert{})
applyCertFilter(r.db, query, filter)
query.Update("alive", false).Joins("Company")
}

View File

@ -1,57 +0,0 @@
package repository
import (
"repo-pattern/app/lib/helpers"
"repo-pattern/app/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
type CompanyRepository struct {
db *gorm.DB
}
type CompanyFilter struct {
Id *uuid.UUID
}
func CreateCompanyRepository(db *gorm.DB) *CompanyRepository {
return &CompanyRepository{db}
}
func applyCompanyFilter(db *gorm.DB, query *gorm.DB, filter *CompanyFilter) *gorm.DB {
if filter.Id != nil {
query.Where("id = ?", *filter.Id)
}
query.Where("is_active = ?", true)
return query
}
func (r *CompanyRepository) New() *models.Company {
now := helpers.UTCNow()
return &models.Company{
IsActive: true,
CreatedAt: now,
UpdatedAt: now,
}
}
func (r *CompanyRepository) Get(filter *CompanyFilter) *models.Company {
var company models.Company
query := r.db.Model(&models.Company{})
applyCompanyFilter(r.db, query, filter)
result := query.First(&company)
if result.Error != nil {
return nil
}
return &company
}
func (r *CompanyRepository) Save(model *models.Company) {
model.UpdatedAt = helpers.UTCNow()
r.db.Save(model)
}

View File

@ -1,23 +0,0 @@
package repository
import "gorm.io/gorm"
type DAO struct {
db *gorm.DB
ApiKeysRepository *ApiKeysRepository
CertRepository *CertRepository
CompanyRepository *CompanyRepository
FiskLogRepository *FiskLogRepository
}
var Dao DAO
func CreateDAO(db *gorm.DB) DAO {
return DAO{
db: db,
ApiKeysRepository: CreateApiKeysRepository(db),
CertRepository: CreateCertRepository(db),
CompanyRepository: CreateCompanyRepository(db),
FiskLogRepository: CreateFiskLogRepository(db),
}
}

View File

@ -1,29 +0,0 @@
package repository
import (
"repo-pattern/app/lib/helpers"
"repo-pattern/app/models"
"gorm.io/gorm"
)
type FiskLogRepository struct {
db *gorm.DB
}
func CreateFiskLogRepository(db *gorm.DB) *FiskLogRepository {
return &FiskLogRepository{db}
}
func (r *FiskLogRepository) New(model models.FiskLogItem) *models.FiskLogItem {
now := helpers.UTCNow()
model.CreatedAt = now
model.UpdatedAt = now
return &model
}
func (r *FiskLogRepository) Save(model *models.FiskLogItem) error {
model.UpdatedAt = helpers.UTCNow()
result := r.db.Save(model)
return result.Error
}

View File

@ -0,0 +1,35 @@
package repository
import (
"errors"
"repo-pattern/app/repository/smartfilter"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
type ExistsMethod[T schema.Tabler] struct {
DbConn *gorm.DB
}
func (m *ExistsMethod[T]) Init(dbConn *gorm.DB) {
m.DbConn = dbConn
}
func (m ExistsMethod[T]) Exists(filter interface{}) (bool, error) {
var (
model T
)
query := m.DbConn.Model(model)
query, err := smartfilter.ToQuery(model, filter, query)
if err != nil {
return false, err
}
result := query.Select("*").First(&model)
exists := !errors.Is(result.Error, gorm.ErrRecordNotFound) && result.Error == nil
return exists, nil
}

View File

@ -0,0 +1,34 @@
package repository
import (
"repo-pattern/app/repository/smartfilter"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
type GetMethod[T schema.Tabler] struct {
DbConn *gorm.DB
}
func (m *GetMethod[T]) Init(dbConn *gorm.DB) {
m.DbConn = dbConn
}
func (m GetMethod[T]) Get(filter interface{}) (*T, error) {
var (
model T
)
query, err := smartfilter.ToQuery(model, filter, m.DbConn)
if err != nil {
return nil, err
}
result := query.First(&model)
if result.Error != nil {
return nil, result.Error
}
return &model, nil
}

View File

@ -0,0 +1,81 @@
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
}
func (m *ListMethod[T]) Init(dbConn *gorm.DB) {
m.DbConn = dbConn
}
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
)
query, err := smartfilter.ToQuery(model, filter, m.DbConn)
if err != nil {
return nil, err
}
query = applyOrdering(query, ordering)
query = applyPagination(query, pagination)
query.Find(&models)
return &models, nil
}

View File

@ -1,84 +0,0 @@
package repository
import (
"errors"
"repo-pattern/app/repository/smartfilter"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
type ListMethod[T schema.Tabler] struct {
DbConn *gorm.DB
}
func (m *ListMethod[T]) Init(dbConn *gorm.DB) {
m.DbConn = dbConn
}
func (m ListMethod[T]) List(filter interface{}) (*[]T, error) {
var (
model T
models []T
)
query, err := smartfilter.ToQuery(model, filter, m.DbConn)
if err != nil {
return nil, err
}
query.Find(&models)
return &models, nil
}
type GetMethod[T schema.Tabler] struct {
DbConn *gorm.DB
}
func (m *GetMethod[T]) Init(dbConn *gorm.DB) {
m.DbConn = dbConn
}
func (m GetMethod[T]) Get(filter interface{}) (*T, error) {
var (
model T
)
query, err := smartfilter.ToQuery(model, filter, m.DbConn)
if err != nil {
return nil, err
}
result := query.First(&model)
if result.Error != nil {
return nil, result.Error
}
return &model, nil
}
type ExistsMethod[T schema.Tabler] struct {
DbConn *gorm.DB
}
func (m *ExistsMethod[T]) Init(dbConn *gorm.DB) {
m.DbConn = dbConn
}
func (m ExistsMethod[T]) Exists(filter interface{}) (bool, error) {
var (
model T
)
query := m.DbConn.Model(model)
query, err := smartfilter.ToQuery(model, filter, query)
if err != nil {
return false, err
}
result := query.Select("*").First(&model)
exists := !errors.Is(result.Error, gorm.ErrRecordNotFound) && result.Error == nil
return exists, nil
}

View File

@ -0,0 +1,239 @@
package repository
import (
"database/sql"
"fmt"
"log"
"regexp"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func NewMockDB() (*sql.DB, *gorm.DB, sqlmock.Sqlmock) {
sqldb, mock, err := sqlmock.New()
if err != nil {
log.Fatalf("An error '%s' was not expected when opening a stub database connection", err)
}
gormdb, err := gorm.Open(postgres.New(postgres.Config{
WithoutQuotingCheck: true,
Conn: sqldb,
}), &gorm.Config{})
if err != nil {
log.Fatalf("An error '%s' was not expected when opening gorm database", err)
}
return sqldb, gormdb, mock
}
type MyModel struct {
Id int
Value string
Count int
}
func (m MyModel) TableName() string {
return "my_models"
}
type MyModelFilter struct {
Id *int `filterfield:"field=id,operator=EQ"`
Value *string `filterfield:"field=value,operator=EQ"`
Count *int `filterfield:"field=count,operator=GT"`
}
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()
repo := RepoBase[MyModel]{}
repo.Init(db)
id := 123
filter := MyModelFilter{
Id: &id,
}
sql := "SELECT * FROM my_models WHERE my_models.id = $1"
mock.ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(sql))).
WithArgs(id)
_, 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", 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,
}
sql := "SELECT * FROM my_models WHERE my_models.id = $1 AND my_models.value = $2 AND my_models.count > $3"
mock.ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(sql))).
WithArgs(id, value, count)
_, 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 {
t.Errorf("there were unfulfilled expectations: %s", err)
}
})
}