Adding sorting lists.
All checks were successful
Deploy Greenlight API / deploy (push) Successful in 58s

This commit is contained in:
Maxime Delporte
2025-11-14 17:31:29 +01:00
parent 6b3a6ee4f5
commit fc2e401f6f
3 changed files with 32 additions and 5 deletions

View File

@@ -1,8 +1,10 @@
<component name="ProjectDictionaryState"> <component name="ProjectDictionaryState">
<dictionary name="project"> <dictionary name="project">
<words> <words>
<w>bigserial</w>
<w>httprouter</w> <w>httprouter</w>
<w>plainto</w> <w>plainto</w>
<w>postgre</w>
<w>servemux</w> <w>servemux</w>
<w>tsvector</w> <w>tsvector</w>
</words> </words>

View File

@@ -1,6 +1,9 @@
package data package data
import "greenlight.craftr.fr/internal/validator" import (
"greenlight.craftr.fr/internal/validator"
"strings"
)
type Filters struct { type Filters struct {
Page int Page int
@@ -19,3 +22,23 @@ func ValidateFilters(v *validator.Validator, f Filters) {
// Check that the sort parameter matches a value in the safelist // Check that the sort parameter matches a value in the safelist
v.Check(validator.PermittedValue(f.Sort, f.SortSafelist...), "sort", "invalid sort value") v.Check(validator.PermittedValue(f.Sort, f.SortSafelist...), "sort", "invalid sort value")
} }
// sortColumn : Check 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 by stripping the leading hyphen character (if one exists)
func (f Filters) sortColumn() string {
for _, safeValue := range f.SortSafelist {
if f.Sort == safeValue {
return strings.TrimPrefix(f.Sort, "-")
}
}
// It will panic if the client-provided 'Sort' value doesn't match one of the entries in our safelist. In theory, this shouldn't happen - the 'Sort' value should have already been checked by calling the 'ValidateFilters()' function - but this is a sensible failsafe to help stop a SQL injection attack occurring
panic("unsafe sort parameter: " + f.Sort)
}
// sortDirection : Return the sort direction ("ASC" or "DESC") depending on the prefix character of the Sort field
func (f Filters) sortDirection() string {
if strings.HasPrefix(f.Sort, "-") {
return "DESC"
}
return "ASC"
}

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
"fmt"
"github.com/lib/pq" "github.com/lib/pq"
"time" "time"
@@ -197,13 +198,14 @@ func (m MovieModel) GetAll(title string, genres []string, filters Filters) ([]*M
// Construct the SQL query to retrieve all movie records. // Construct the SQL query to retrieve all movie records.
// to_tsvector('simple', title) transforms 'The Breakfast Club' into 'breakfast' 'club' 'the'. The 'simple' parameter's value is the configuration. // to_tsvector('simple', title) transforms 'The Breakfast Club' into 'breakfast' 'club' 'the'. The 'simple' parameter's value is the configuration.
// plainto_tsquery('simple', $1) takes a search value and turns it into a formatted query term that PostgreSQL full-text search can understand. As an example : "The Club" would result in the query term 'the' & 'club' // plainto_tsquery('simple', $1) takes a search value and turns it into a formatted query term that PostgreSQL full-text search can understand. As an example : "The Club" would result in the query term 'the' & 'club'
// The @@ operator is the matches operator. To continue the example, the query term 'the' & 'club' will match rows which contain both lexemes 'the' and 'club'. // The @@ operator is the matches' operator. To continue the example, the query term 'the' & 'club' will match rows which contain both lexemes 'the' and 'club'.
query := ` // Add an ORDER BY clause and interpolate the sort column and direction. Importantly, notice that we also include a secondary sort on the movie ID to ensure a consistent ordering.
query := fmt.Sprintf(`
SELECT id, created_at, title, year, runtime, genres, version SELECT id, created_at, title, year, runtime, genres, version
FROM movies FROM movies
WHERE (to_tsvector('simple', title) @@ plainto_tsquery('simple', $1) OR $1 = '') WHERE (to_tsvector('simple', title) @@ plainto_tsquery('simple', $1) OR $1 = '')
AND (genres @> $2 OR $2 = '{}') AND (genres @> $2 OR $2 = '{}')
ORDER BY id` ORDER BY %s %s, id ASC`, filters.sortColumn(), filters.sortDirection())
// Create a context with a 3-second timeout // Create a context with a 3-second timeout
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
@@ -219,7 +221,7 @@ func (m MovieModel) GetAll(title string, genres []string, filters Filters) ([]*M
defer rows.Close() defer rows.Close()
// Initialize an empty slice to hold the movie data. // Initialize an empty slice to hold the movie data.
movies := []*Movie{} var movies []*Movie
// Use rows.Next to iterate through the rows in the resultset. // Use rows.Next to iterate through the rows in the resultset.
for rows.Next() { for rows.Next() {