Files
ersteller/user/user_repository.go
T
2025-11-15 11:57:09 +01:00

401 lines
8.8 KiB
Go

package user
import (
"context"
"encoding/json"
"errors"
"ersteller-lib"
"fmt"
"github.com/doug-martin/goqu/v9"
_ "github.com/doug-martin/goqu/v9/dialect/postgres"
"github.com/doug-martin/goqu/v9/exp"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"strings"
"time"
)
type UserRepository struct {
connPool *pgxpool.Pool
dialect goqu.DialectWrapper
}
func NewUserRepository(connPool *pgxpool.Pool) *UserRepository {
return &UserRepository{
connPool: connPool,
dialect: goqu.Dialect("postgres"),
}
}
func (r *UserRepository) Create(user User) (int, error) {
sql, args, err := r.dialect.Insert("user").
Prepared(true).
Rows(goqu.Record{
"updated_at": time.Now(),
"email": user.Email,
"state": r.jsonToString(user.State),
"admin": user.Admin,
"password": user.Password,
}).
Returning("id").
ToSQL()
if err != nil {
ersteller_lib.LogError("error creating create User sql: %v", err)
return -1, err
}
rows, err := r.connPool.Query(context.Background(), sql, args...)
if err != nil {
ersteller_lib.LogError("error creating User: %v", err)
return -1, err
}
defer rows.Close()
var id int
if rows.Next() {
err = rows.Scan(&id)
if err != nil {
ersteller_lib.LogError("error scanning User: %v", err)
return -1, err
}
} else {
ersteller_lib.Error("User already exists")
return -1, UserAlreadyExistsError{User: user}
}
return id, nil
}
type UserAlreadyExistsError struct {
User User
}
func (e UserAlreadyExistsError) Error() string {
return fmt.Sprint("User ", e.User, " already exists")
}
func (r *UserRepository) getSelectColumns() []any {
return []any{"id", "created_at", "updated_at",
"email", "state", "admin", "password",
}
}
func (r *UserRepository) Read(id int) (User, error) {
ersteller_lib.Debug("Getting User by id ", id)
sql, args, _ := r.dialect.From("user").
Prepared(true).
Select(r.getSelectColumns()...).
Where(goqu.Ex{
"id": id,
}).
ToSQL()
rows, err := r.connPool.Query(context.Background(), sql, args...)
if err != nil {
ersteller_lib.Error("Failed to get User: ", err)
}
defer rows.Close()
if rows.Next() {
item, _, err := r.rowToItem(rows, false)
return item, err
}
return User{}, errors.New("no rows found")
}
type UserItemScan struct {
User
RowId int
Count int
}
func (r *UserRepository) rowToItem(rows pgx.Rows, rowId bool) (User, int, error) {
var item UserItemScan
if rowId {
err := rows.Scan(
&item.RowId,
&item.Count,
&item.Id,
&item.CreatedAt,
&item.UpdatedAt,
&item.Email,
&item.State,
&item.Admin,
&item.Password,
)
if err != nil {
return User{}, -1, err
}
} else {
err := rows.Scan(
&item.Id,
&item.CreatedAt,
&item.UpdatedAt,
&item.Email,
&item.State,
&item.Admin,
&item.Password,
)
if err != nil {
return User{}, -1, err
}
}
return User{
Id: item.Id,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
Email: item.Email,
State: item.State,
Admin: item.Admin,
Password: item.Password,
}, item.Count, nil
}
func (r *UserRepository) Update(user User) error {
sql, args, err := r.dialect.Update("user").
Prepared(true).
Set(goqu.Record{
"updated_at": time.Now(),
"email": user.Email,
"state": r.jsonToString(user.State),
"admin": user.Admin,
"password": user.Password,
}).
Where(goqu.Ex{
"id": user.Id,
}).
ToSQL()
if err != nil {
ersteller_lib.LogError("error creating update User sql: %v", err)
return err
}
_, err = r.connPool.Exec(context.Background(), sql, args...)
if err != nil {
ersteller_lib.LogError("error updating User: %v", err)
return err
}
return nil
}
func (r *UserRepository) Delete(id int) error {
sql, args, err := r.dialect.Delete("user").
Prepared(true).
Where(goqu.Ex{
"id": id,
}).
ToSQL()
if err != nil {
ersteller_lib.LogError("error creating delete User sql: %v", err)
return err
}
_, err = r.connPool.Exec(context.Background(), sql, args...)
if err != nil {
ersteller_lib.LogError("error deleting User: %v", err)
return err
}
return nil
}
type UserField string
const (
UserFieldEmail UserField = "email"
UserFieldState UserField = "state"
UserFieldAdmin UserField = "admin"
UserFieldPassword UserField = "password"
)
type UserEmailFilter struct {
Active bool
Value string
}
type UserAdminFilter struct {
Active bool
Value bool
}
type UserPasswordFilter struct {
Active bool
Value string
}
type UserOrderDirection string
const (
UserOrderDirectionAsc UserOrderDirection = "asc"
UserOrderDirectionDesc UserOrderDirection = "desc"
)
type UserPaginationParams struct {
RowId int
PageSize int
OrderBy UserField
OrderDirection UserOrderDirection
EmailFilter UserEmailFilter
AdminFilter UserAdminFilter
PasswordFilter UserPasswordFilter
}
func (r *UserRepository) GetPage(params UserPaginationParams) ([]User, int, error) {
var orderByWindow exp.WindowExpression
if params.OrderDirection == UserOrderDirectionAsc {
orderByWindow = goqu.W().OrderBy(goqu.C(string(params.OrderBy)).Asc())
} else {
orderByWindow = goqu.W().OrderBy(goqu.C(string(params.OrderBy)).Desc())
}
selectColumns := []any{
goqu.ROW_NUMBER().Over(orderByWindow).As("row_id"),
goqu.COUNT("*"),
}
selectColumns = append(selectColumns, r.getSelectColumns()...)
whereExpressions := []goqu.Expression{
goqu.Ex{},
}
whereExpressions = r.addPageFilters(params, whereExpressions)
var colOrder exp.OrderedExpression
if params.OrderDirection == UserOrderDirectionAsc {
colOrder = goqu.C(string(params.OrderBy)).Asc()
} else {
colOrder = goqu.C(string(params.OrderBy)).Desc()
}
dialect := goqu.Dialect("postgres")
innerFrom := dialect.From("user").
Prepared(true).
Select(selectColumns...).
Where(whereExpressions...).
Order(colOrder)
outerFrom := dialect.From(innerFrom).
Prepared(true).
Where(goqu.Ex{"row_id": goqu.Op{"gt": params.RowId}})
if params.PageSize > 0 {
outerFrom = outerFrom.Limit(uint(params.PageSize))
}
sql, args, _ := outerFrom.ToSQL()
sql = strings.Replace(sql, "COUNT(*)", "COUNT(*) over()", 1)
rows, err := r.connPool.Query(context.Background(), sql, args...)
if err != nil {
ersteller_lib.LogError("failed to run sql query: %v", err)
return nil, -1, err
}
defer rows.Close()
results := make([]User, 0)
totalCount := 0
for rows.Next() {
parsed, count, err := r.rowToItem(rows, true)
if err != nil {
return nil, -1, err
}
totalCount = count
results = append(results, parsed)
}
return results, totalCount, nil
}
func (r *UserRepository) addPageFilters(params UserPaginationParams, whereExpressions []goqu.Expression) []goqu.Expression {
if params.EmailFilter.Active {
whereExpressions = append(whereExpressions, goqu.Ex{
"email": goqu.Op{"ilike": fmt.Sprint("%", params.EmailFilter.Value, "%")},
})
}
if params.AdminFilter.Active {
whereExpressions = append(whereExpressions, goqu.Ex{
"admin": params.AdminFilter.Value,
})
}
if params.PasswordFilter.Active {
whereExpressions = append(whereExpressions, goqu.Ex{
"password": goqu.Op{"ilike": fmt.Sprint("%", params.PasswordFilter.Value, "%")},
})
}
return whereExpressions
}
func (r *UserRepository) jsonToString(jsonData any) string {
bytes, err := json.Marshal(jsonData)
if err != nil {
return "{}"
}
return string(bytes)
}
func (r *UserRepository) DoesUserEmailExist(email string) (bool, error) {
sql, args, _ := r.dialect.From("user").
Prepared(true).
Select(goqu.COUNT("email")).
Where(goqu.Ex{"email": email}).
ToSQL()
rows, err := r.connPool.Query(context.Background(), sql, args...)
if err != nil {
ersteller_lib.LogError("failed to run sql query: %v", err)
return false, err
}
defer rows.Close()
if rows.Next() {
var count int
err = rows.Scan(&count)
if err != nil {
return false, err
}
return count == 1, nil
}
return false, nil
}
func (r *UserRepository) GetUserId(email string) (int, error) {
sql, args, _ := r.dialect.From("user").
Prepared(true).
Select("id").
Where(goqu.Ex{"email": email}).
ToSQL()
rows, err := r.connPool.Query(context.Background(), sql, args...)
if err != nil {
ersteller_lib.LogError("failed to run sql query: %v", err)
return -1, err
}
defer rows.Close()
if rows.Next() {
var id int
err = rows.Scan(&id)
if err != nil {
return -1, err
}
return id, nil
}
return -1, errors.New("did not find user with email " + email)
}
func (r *UserRepository) CreateFromEmail(email string) (int, error) {
return r.Create(User{
Email: email,
})
}
func (r *UserRepository) VerifyPassword(email string, password string) (bool, int, error) {
userId, err := r.GetUserId(email)
if err != nil {
return false, -1, err
}
user, err := r.Read(userId)
if err != nil {
return false, -1, err
}
return ersteller_lib.VerifyPassword(password, user.Password), userId, nil
}