Adding sort to filter

main
Drew Bednar 4 days ago
parent 9070d4a4c8
commit 7062e86c5b

@ -33,6 +33,25 @@ BODY='{"title":"Black Panther","year":2018,"runtime":"134 mins","genres":["sci-f
curl -X PUT -d "$BODY" http://0.0.0.0:5002/v1/movies/1
```
## Sorting by items
REMEMBER: you have to quote strings that contain &
```bash
curl http://localhost:5002/v1/movies?genres=comedy&sort=-year
```
```text
"time":"2026-02-08T11:35:42.782856326-05:00","level":"DEBUG","source":{"function":"main.(*application).listMoviesHandler","file":"/home/toor/workspace/pulley/cmd/api/handlers.go","line":247},"msg":"query params","qp":{"genres":["comedy"]}}
```
```bash
curl "http://localhost:5002/v1/movies?genres=comedy&sort=-year"
```
```text
{"time":"2026-02-08T11:38:52.253316749-05:00","level":"DEBUG","source":{"function":"main.(*application).listMoviesHandler","file":"/home/toor/workspace/pulley/cmd/api/handlers.go","line":247},"msg":"query params","qp":{"genres":["comedy"],"sort":["-year"]}}
```
## Generating an Update Conflict
We can try to use the xargs tool to submit multiple requests to our webserver. Hopefully this results in the 409 that we are looking to create.

@ -92,7 +92,7 @@ func run(ctx context.Context, w io.Writer, args []string) error {
pool.Close()
}()
models := data.NewModels(pool)
models := data.NewModels(pool, logger)
app := application{config: cfg, logger: logger, models: models}

@ -11,11 +11,10 @@ import (
func newTestApplication(pool database.PgxIface) application {
cfg := config.ServiceConfig{Env: "test"}
mockModels := data.NewModels(pool)
// Discards log output from tests
// logger := slog.New(slog.NewTextHandler(io.Discard, nil))
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true}))
mockModels := data.NewModels(pool, logger)
return application{config: cfg, logger: logger, models: mockModels}
}

@ -1,6 +1,10 @@
package data
import "git.runcible.io/learning/pulley/internal/validator"
import (
"strings"
"git.runcible.io/learning/pulley/internal/validator"
)
type Filters struct {
Page int
@ -16,3 +20,24 @@ func ValidateFilters(v *validator.Validator, f Filters) {
v.Check(f.PageSize <= 100, "page_size", "must be a maximum of 100")
v.Check(validator.PermittedValue(f.Sort, f.SortSafelist...), "sort", "invalid sort value")
}
// sortColumn checks that the client-provided Sort field matches one of the entries in our safelist
// and if it does, extract the column name from the Sort field.
func (f Filters) sortColumn() string {
for _, safeValue := range f.SortSafelist {
if f.Sort == safeValue {
return strings.TrimPrefix(f.Sort, "-")
}
}
// ValidateFilters should have checked this, but we are extra careful of sql injection.
panic("unsafe sort parameter: " + f.Sort)
}
// sortDirection returns the sort direction depending on the prefix
// character of the Sort field.
func (f Filters) sortDirection() string {
if strings.HasPrefix(f.Sort, "-") {
return "DESC"
}
return "ASC"
}

@ -2,6 +2,7 @@ package data
import (
"errors"
"log/slog"
"git.runcible.io/learning/pulley/internal/database"
)
@ -16,8 +17,8 @@ type Models struct {
Movies MovieModel
}
func NewModels(pool database.PgxIface) *Models {
func NewModels(pool database.PgxIface, logger *slog.Logger) *Models {
return &Models{
Movies: MovieModel{db: pool},
Movies: MovieModel{db: pool, logger: logger},
}
}

@ -3,6 +3,8 @@ package data
import (
"context"
"errors"
"fmt"
"log/slog"
"time"
"git.runcible.io/learning/pulley/internal/database"
@ -31,6 +33,7 @@ type Movie struct {
type MovieModel struct {
db database.PgxIface
logger *slog.Logger
}
func (m MovieModel) Insert(ctx context.Context, movie *Movie) error {
@ -189,12 +192,19 @@ func (m MovieModel) GetAll(ctx context.Context, title string, genres []string, f
// example, the query term 'the' & 'club' will match rows which contain both lexemes
// 'the' and 'club'.
query := `
// query := `
// SELECT id, created_at, title, year, runtime, genres, version
// FROM movies
// WHERE (to_tsvector('simple', title) @@ plainto_tsquery('simple', $1) OR $1 = '')
// AND (genres @> $2 OR $2 = '{}')
// ORDER BY id`
query := fmt.Sprintf(`
SELECT id, created_at, title, year, runtime, genres, version
FROM movies
WHERE (to_tsvector('simple', title) @@ plainto_tsquery('simple', $1) OR $1 = '')
AND (genres @> $2 OR $2 = '{}')
ORDER BY id`
ORDER BY %s %s`, filters.sortColumn(), filters.sortDirection())
// ctx want some timeout for queries. When used in the handler the context passed should
// be the r.Context. Since cancel functions are inherited it will cancel on client
@ -202,6 +212,8 @@ func (m MovieModel) GetAll(ctx context.Context, title string, genres []string, f
ctxTimeout, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
m.logger.Debug(query)
rows, err := m.db.Query(ctxTimeout, query, title, genres)
if err != nil {
return nil, err

Loading…
Cancel
Save