Skip to content

Commit

Permalink
Support pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
Roma7-7-7 committed Feb 21, 2024
1 parent b556f55 commit 15aff99
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 20 deletions.
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ require (
github.com/lib/pq v1.10.9
go.etcd.io/bbolt v1.3.8
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.15.0
golang.org/x/crypto v0.19.0
)

require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/sys v0.17.0 // indirect
)
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
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/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
Expand All @@ -26,9 +26,9 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
88 changes: 88 additions & 0 deletions internal/dal/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ type (
UpdatedAt time.Time
}

SessionFilter struct {
userID uint64
limit int
name string
sortBy string
sortByDirection string
offset int
}

Session struct {
ID uint64
Name string
Expand All @@ -27,3 +36,82 @@ type (
UpdatedAt time.Time
}
)

func (s SessionFilter) UserID() uint64 {
return s.userID
}

func (s SessionFilter) Limit() int {
return s.limit
}

func (s SessionFilter) Name() string {
return s.name
}

func (s SessionFilter) SortBy() string {
return s.sortBy
}

func (s SessionFilter) SortByDirection() string {
return s.sortByDirection
}

func (s SessionFilter) Offset() int {
return s.offset
}

func WithName(name string) func(SessionFilter) SessionFilter {
return func(f SessionFilter) SessionFilter {
f.name = name
return f
}
}

func WithSortByName(desc bool) func(SessionFilter) SessionFilter {
return func(f SessionFilter) SessionFilter {
f.sortBy = "name"
if desc {
f.sortByDirection = "DESC"
} else {
f.sortByDirection = "ASC"
}
return f
}
}

func WithSortByUpdateAt(desc bool) func(SessionFilter) SessionFilter {
return func(f SessionFilter) SessionFilter {
f.sortBy = "updated_at"
if desc {
f.sortByDirection = "DESC"
} else {
f.sortByDirection = "ASC"
}
return f
}
}

func WithOffset(offset int) func(SessionFilter) SessionFilter {
return func(f SessionFilter) SessionFilter {
f.offset = offset
return f
}
}

func NewSessionFilter(userID uint64, limit int, opts ...func(SessionFilter) SessionFilter) SessionFilter {
f := SessionFilter{
userID: userID,
limit: limit,
name: "",
sortBy: "updated_at",
sortByDirection: "ASC",
offset: 0,
}

for _, opt := range opts {
f = opt(f)
}

return f
}
99 changes: 96 additions & 3 deletions internal/dal/postgre/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import (
"database/sql"
"errors"
"fmt"
"sync"

"github.com/Roma7-7-7/shared-clipboard/internal/dal"
)

type SessionRepository struct {
db *sql.DB
}
type (
SessionRepository struct {
db *sql.DB
}
)

func NewSessionRepository(db *sql.DB) (*SessionRepository, error) {
return &SessionRepository{
Expand Down Expand Up @@ -71,6 +74,75 @@ func (r *SessionRepository) GetAllByUserID(userID uint64) ([]*dal.Session, error
return res, nil
}

func (r *SessionRepository) FilterBy(filter dal.SessionFilter) ([]*dal.Session, int, error) {
var (
totalCount = 0
res = make([]*dal.Session, 0, 10)
filterQuery = "user_id = $1 AND ($2 = '' OR name LIKE $2)"
totalCountQuery = "SELECT COUNT(*) FROM sessions WHERE " + filterQuery
query = fmt.Sprintf("SELECT session_id, user_id, name, created_at, updated_at FROM sessions WHERE "+filterQuery+" ORDER BY %s %s OFFSET $3 LIMIT $4", filter.SortBy(), filter.SortByDirection())
totalCountErr error
queryErr error
)

wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
if err := r.db.QueryRow(totalCountQuery, filter.UserID(), "%"+filter.Name()+"%").Scan(&totalCount); err != nil {
if errors.Is(err, sql.ErrNoRows) {
totalCount = 0
return
}

totalCountErr = fmt.Errorf("get total count: %w", err)
}
}()
go func() {
defer wg.Done()
rows, err := r.db.Query(query, filter.UserID(), "%"+filter.Name()+"%", filter.Offset(), filter.Limit())
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return
}

queryErr = fmt.Errorf("get sessions: %w", err)
return
}
defer rows.Close()

for rows.Next() {
var s dal.Session

if err = rows.Scan(
&s.ID,
&s.UserID,
&s.Name,
&s.CreatedAt,
&s.UpdatedAt,
); err != nil {
queryErr = fmt.Errorf("scan session: %w", err)
return
}

res = append(res, &s)
}
}()

wg.Wait()
if totalCountErr != nil && queryErr != nil {
return nil, 0, fmt.Errorf("filter sessions: %w; %w", totalCountErr, queryErr)
}
if totalCountErr != nil {
return nil, 0, fmt.Errorf("filter sessions: %w", totalCountErr)
}
if queryErr != nil {
return nil, 0, fmt.Errorf("filter sessions: %w", queryErr)
}

return res, totalCount, nil
}

func (r *SessionRepository) Create(name string, userID uint64) (*dal.Session, error) {
res := &dal.Session{
UserID: userID,
Expand Down Expand Up @@ -115,6 +187,27 @@ func (r *SessionRepository) Update(id uint64, name string) (*dal.Session, error)
return r.GetByID(id)
}

func (r *SessionRepository) UpdateUpdatedAt(id uint64) error {
execRes, err := r.db.Exec("UPDATE sessions SET updated_at = now() WHERE session_id = $1", id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("session with session_id=%d not found: %w", id, dal.ErrNotFound)
}

return fmt.Errorf("update session: %w", err)
}

affected, err := execRes.RowsAffected()
if err != nil {
return fmt.Errorf("get affected rows: %w", err)
}
if affected == 0 {
return fmt.Errorf("session with session_id=%d not found: %w", id, dal.ErrNotFound)
}

return nil
}

func (r *SessionRepository) Delete(id uint64) error {
execRes, err := r.db.Exec("DELETE FROM sessions WHERE session_id = $1", id)
if err != nil {
Expand Down
50 changes: 50 additions & 0 deletions internal/domain/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ var (
)

type (
SessionFilter struct {
Limit int `validate:"required,gte=1,lte=100"`
Name string
SortBy string `validate:"omitempty,oneof=name updated_at"`
SortByDesc bool
Offset int `validate:"gte=0"`
}

Session struct {
ID uint64
Name string
Expand All @@ -28,8 +36,10 @@ type (
SessionRepository interface {
GetByID(id uint64) (*dal.Session, error)
GetAllByUserID(userID uint64) ([]*dal.Session, error)
FilterBy(dal.SessionFilter) ([]*dal.Session, int, error)
Create(name string, userID uint64) (*dal.Session, error)
Update(id uint64, name string) (*dal.Session, error)
UpdateUpdatedAt(id uint64) error
Delete(id uint64) error
}

Expand Down Expand Up @@ -83,6 +93,35 @@ func (s *SessionService) GetByUserID(ctx context.Context, userID uint64) ([]*Ses
return res, nil
}

func (s *SessionService) FilterBy(ctx context.Context, userID uint64, filter SessionFilter) ([]*Session, int, error) {
s.log.Debugw(ctx, "filter sessions", "userID", userID, "filter", filter)

filterOpts := make([]func(dal.SessionFilter) dal.SessionFilter, 0, 3)
if filter.Name != "" {
filterOpts = append(filterOpts, dal.WithName(filter.Name))
}
switch filter.SortBy {
case "name":
filterOpts = append(filterOpts, dal.WithSortByName(filter.SortByDesc))
case "updated_at":
filterOpts = append(filterOpts, dal.WithSortByUpdateAt(filter.SortByDesc))
}
filterOpts = append(filterOpts, dal.WithOffset(filter.Offset))
dalFilter := dal.NewSessionFilter(userID, filter.Limit, filterOpts...)

sessions, total, err := s.sessionRepo.FilterBy(dalFilter)
if err != nil {
return nil, 0, fmt.Errorf("filter sessions by %v: %w", filter, err)
}

s.log.Debugw(ctx, "sessions found", "count", len(sessions), "total", total)
res := make([]*Session, 0, len(sessions))
for _, session := range sessions {
res = append(res, toSession(session))
}
return res, total, nil
}

func (s *SessionService) Create(ctx context.Context, userID uint64, name string) (*Session, error) {
s.log.Debugw(ctx, "create session", "name", name, "userID", userID)

Expand Down Expand Up @@ -128,6 +167,17 @@ func (s *SessionService) Update(ctx context.Context, userID, sessionID uint64, n
return toSession(updated), nil
}

func (s *SessionService) UpdateUpdatedAt(ctx context.Context, sessionID uint64) error {
s.log.Debugw(ctx, "update session updated_at", "sessionID", sessionID)

if err := s.sessionRepo.UpdateUpdatedAt(sessionID); err != nil {
return fmt.Errorf("update session updated_at by id=%d: %w", sessionID, err)
}

s.log.Debugw(ctx, "session updated_at updated", "sessionID", sessionID)
return nil
}

func (s *SessionService) Delete(ctx context.Context, userID, sessionID uint64) error {
s.log.Debugw(ctx, "delete session", "sessionID", sessionID)

Expand Down
7 changes: 6 additions & 1 deletion internal/handle/respond.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ type genericErrorResponse struct {
Details any `json:"details,omitempty"`
}

type paginatedResponse struct {
Items any `json:"items"`
TotalItems int `json:"totalItems"`
}

type responder struct {
log log.TracedLogger
}
Expand All @@ -46,7 +51,7 @@ func (r *responder) Send(ctx context.Context, rw http.ResponseWriter, status int
}
}
if !contentTypeSet {
rw.Header().Set(ContentTypeJSON, ContentTypeJSON)
rw.Header().Set(ContentTypeHeader, ContentTypeJSON)
}
rw.WriteHeader(status)

Expand Down
2 changes: 1 addition & 1 deletion internal/handle/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func NewRouter(ctx context.Context, deps Dependencies, log log.TracedLogger) (*c

sessionHandler := NewSessionHandler(deps.SessionService, deps.ClipboardRepository, resp, log)
authorizedRouter.Post("/v1/sessions", sessionHandler.Create)
authorizedRouter.Get("/v1/sessions", sessionHandler.GetAllByUserID)
authorizedRouter.Get("/v1/sessions", sessionHandler.FilterBy)
authorizedRouter.Get("/v1/sessions/{sessionID}", sessionHandler.GetByID)
authorizedRouter.Put("/v1/sessions/{sessionID}", sessionHandler.Update)
authorizedRouter.Delete("/v1/sessions/{sessionID}", sessionHandler.Delete)
Expand Down
Loading

0 comments on commit 15aff99

Please sign in to comment.