mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-07-30 06:07:38 +00:00
feat(handler): implement default sort configuration for models
* Add SetDefaultSort method to configure default sort order * Implement getDefaultSort method to retrieve configured defaults * Update handleRead to apply default sort when none specified * Add tests for default sort functionality
This commit is contained in:
@@ -90,6 +90,30 @@ type SortOption struct {
|
|||||||
Direction string `json:"direction"`
|
Direction string `json:"direction"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrimaryKeySortColumn is a sentinel SortOption.Column value that gets
|
||||||
|
// resolved to a model's actual primary key column name at query time.
|
||||||
|
// This lets a single global default sort (e.g. set once via
|
||||||
|
// Handler.SetDefaultSort) work across models with different primary keys,
|
||||||
|
// e.g. common.SortOption{Column: common.PrimaryKeySortColumn, Direction: "asc"}.
|
||||||
|
const PrimaryKeySortColumn = "$pk"
|
||||||
|
|
||||||
|
// ResolveSortColumns returns a copy of sort with any PrimaryKeySortColumn
|
||||||
|
// entries replaced by pkName. If pkName is empty, matching entries are
|
||||||
|
// dropped since there is no column to sort by.
|
||||||
|
func ResolveSortColumns(sort []SortOption, pkName string) []SortOption {
|
||||||
|
resolved := make([]SortOption, 0, len(sort))
|
||||||
|
for _, s := range sort {
|
||||||
|
if s.Column == PrimaryKeySortColumn {
|
||||||
|
if pkName == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.Column = pkName
|
||||||
|
}
|
||||||
|
resolved = append(resolved, s)
|
||||||
|
}
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
|
||||||
type CustomOperator struct {
|
type CustomOperator struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
SQL string `json:"sql"`
|
SQL string `json:"sql"`
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolveSortColumns(t *testing.T) {
|
||||||
|
sort := []SortOption{
|
||||||
|
{Column: PrimaryKeySortColumn, Direction: "asc"},
|
||||||
|
{Column: "name", Direction: "desc"},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := ResolveSortColumns(sort, "user_id")
|
||||||
|
want := []SortOption{
|
||||||
|
{Column: "user_id", Direction: "asc"},
|
||||||
|
{Column: "name", Direction: "desc"},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("ResolveSortColumns() = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original slice must not be mutated
|
||||||
|
if sort[0].Column != PrimaryKeySortColumn {
|
||||||
|
t.Errorf("ResolveSortColumns() mutated input slice: %v", sort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveSortColumnsEmptyPK(t *testing.T) {
|
||||||
|
sort := []SortOption{
|
||||||
|
{Column: PrimaryKeySortColumn, Direction: "asc"},
|
||||||
|
{Column: "name", Direction: "desc"},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := ResolveSortColumns(sort, "")
|
||||||
|
want := []SortOption{{Column: "name", Direction: "desc"}}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("ResolveSortColumns() with empty pk = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveSortColumnsNil(t *testing.T) {
|
||||||
|
if got := ResolveSortColumns(nil, "id"); len(got) != 0 {
|
||||||
|
t.Errorf("ResolveSortColumns(nil) = %v, want empty", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ type Handler struct {
|
|||||||
hooks *HookRegistry
|
hooks *HookRegistry
|
||||||
fallbackHandler FallbackHandler
|
fallbackHandler FallbackHandler
|
||||||
openAPIGenerator func() (string, error)
|
openAPIGenerator func() (string, error)
|
||||||
|
defaultSort map[string][]common.SortOption
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a new API handler with database and registry abstractions
|
// NewHandler creates a new API handler with database and registry abstractions
|
||||||
@@ -56,6 +57,32 @@ func (h *Handler) SetFallbackHandler(fallback FallbackHandler) {
|
|||||||
h.fallbackHandler = fallback
|
h.fallbackHandler = fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDefaultSort sets the sort order applied to list ("read") results when a
|
||||||
|
// request does not specify its own sort. Pass an empty schema and name to set
|
||||||
|
// a global default used by any model without a more specific default.
|
||||||
|
func (h *Handler) SetDefaultSort(schema, name string, sort ...common.SortOption) {
|
||||||
|
if h.defaultSort == nil {
|
||||||
|
h.defaultSort = make(map[string][]common.SortOption)
|
||||||
|
}
|
||||||
|
h.defaultSort[defaultSortKey(schema, name)] = sort
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultSort returns the configured default sort for a model, falling
|
||||||
|
// back to the global default (registered with an empty schema and name).
|
||||||
|
func (h *Handler) getDefaultSort(schema, name string) []common.SortOption {
|
||||||
|
if h.defaultSort == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if sort, ok := h.defaultSort[defaultSortKey(schema, name)]; ok {
|
||||||
|
return sort
|
||||||
|
}
|
||||||
|
return h.defaultSort[defaultSortKey("", "")]
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultSortKey(schema, name string) string {
|
||||||
|
return fmt.Sprintf("%s.%s", schema, name)
|
||||||
|
}
|
||||||
|
|
||||||
// GetDatabase returns the underlying database connection
|
// GetDatabase returns the underlying database connection
|
||||||
// Implements common.SpecHandler interface
|
// Implements common.SpecHandler interface
|
||||||
func (h *Handler) GetDatabase() common.Database {
|
func (h *Handler) GetDatabase() common.Database {
|
||||||
@@ -345,6 +372,11 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
|||||||
query = query.Where(customOp.SQL)
|
query = query.Where(customOp.SQL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fall back to the configured default sort when the request didn't specify one
|
||||||
|
if len(options.Sort) == 0 {
|
||||||
|
options.Sort = common.ResolveSortColumns(h.getDefaultSort(schema, entity), reflection.GetPrimaryKeyName(model))
|
||||||
|
}
|
||||||
|
|
||||||
// Apply sorting
|
// Apply sorting
|
||||||
for _, sort := range options.Sort {
|
for _, sort := range options.Sort {
|
||||||
direction := "ASC"
|
direction := "ASC"
|
||||||
|
|||||||
@@ -41,6 +41,36 @@ func TestSetFallbackHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSetDefaultSort(t *testing.T) {
|
||||||
|
handler := NewHandler(nil, nil)
|
||||||
|
|
||||||
|
// No default configured yet
|
||||||
|
if got := handler.getDefaultSort("public", "users"); got != nil {
|
||||||
|
t.Errorf("Expected no default sort, got %v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global default
|
||||||
|
global := []common.SortOption{{Column: "created_at", Direction: "desc"}}
|
||||||
|
handler.SetDefaultSort("", "", global...)
|
||||||
|
|
||||||
|
if got := handler.getDefaultSort("public", "users"); !reflect.DeepEqual(got, global) {
|
||||||
|
t.Errorf("Expected global default sort %v, got %v", global, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-model default overrides the global default
|
||||||
|
perModel := []common.SortOption{{Column: "name", Direction: "asc"}}
|
||||||
|
handler.SetDefaultSort("public", "users", perModel...)
|
||||||
|
|
||||||
|
if got := handler.getDefaultSort("public", "users"); !reflect.DeepEqual(got, perModel) {
|
||||||
|
t.Errorf("Expected per-model default sort %v, got %v", perModel, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other models still fall back to the global default
|
||||||
|
if got := handler.getDefaultSort("public", "orders"); !reflect.DeepEqual(got, global) {
|
||||||
|
t.Errorf("Expected global default sort %v for unrelated model, got %v", global, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetDatabase(t *testing.T) {
|
func TestGetDatabase(t *testing.T) {
|
||||||
handler := NewHandler(nil, nil)
|
handler := NewHandler(nil, nil)
|
||||||
db := handler.GetDatabase()
|
db := handler.GetDatabase()
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package restheadspec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetDefaultSort(t *testing.T) {
|
||||||
|
handler := NewHandler(nil, nil)
|
||||||
|
|
||||||
|
// No default configured yet
|
||||||
|
if got := handler.getDefaultSort("public", "users"); got != nil {
|
||||||
|
t.Errorf("Expected no default sort, got %v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global default
|
||||||
|
global := []common.SortOption{{Column: "created_at", Direction: "desc"}}
|
||||||
|
handler.SetDefaultSort("", "", global...)
|
||||||
|
|
||||||
|
if got := handler.getDefaultSort("public", "users"); !reflect.DeepEqual(got, global) {
|
||||||
|
t.Errorf("Expected global default sort %v, got %v", global, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-model default overrides the global default
|
||||||
|
perModel := []common.SortOption{{Column: "name", Direction: "asc"}}
|
||||||
|
handler.SetDefaultSort("public", "users", perModel...)
|
||||||
|
|
||||||
|
if got := handler.getDefaultSort("public", "users"); !reflect.DeepEqual(got, perModel) {
|
||||||
|
t.Errorf("Expected per-model default sort %v, got %v", perModel, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other models still fall back to the global default
|
||||||
|
if got := handler.getDefaultSort("public", "orders"); !reflect.DeepEqual(got, global) {
|
||||||
|
t.Errorf("Expected global default sort %v for unrelated model, got %v", global, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ type Handler struct {
|
|||||||
nestedProcessor *common.NestedCUDProcessor
|
nestedProcessor *common.NestedCUDProcessor
|
||||||
fallbackHandler FallbackHandler
|
fallbackHandler FallbackHandler
|
||||||
openAPIGenerator func() (string, error)
|
openAPIGenerator func() (string, error)
|
||||||
|
defaultSort map[string][]common.SortOption
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a new API handler with database and registry abstractions
|
// NewHandler creates a new API handler with database and registry abstractions
|
||||||
@@ -64,6 +65,33 @@ func (h *Handler) SetFallbackHandler(fallback FallbackHandler) {
|
|||||||
h.fallbackHandler = fallback
|
h.fallbackHandler = fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDefaultSort sets the sort order applied to list ("read") results when a
|
||||||
|
// request does not specify its own sort (no x-sort header / sort query param).
|
||||||
|
// Pass an empty schema and name to set a global default used by any model
|
||||||
|
// without a more specific default.
|
||||||
|
func (h *Handler) SetDefaultSort(schema, name string, sort ...common.SortOption) {
|
||||||
|
if h.defaultSort == nil {
|
||||||
|
h.defaultSort = make(map[string][]common.SortOption)
|
||||||
|
}
|
||||||
|
h.defaultSort[defaultSortKey(schema, name)] = sort
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultSort returns the configured default sort for a model, falling
|
||||||
|
// back to the global default (registered with an empty schema and name).
|
||||||
|
func (h *Handler) getDefaultSort(schema, name string) []common.SortOption {
|
||||||
|
if h.defaultSort == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if sort, ok := h.defaultSort[defaultSortKey(schema, name)]; ok {
|
||||||
|
return sort
|
||||||
|
}
|
||||||
|
return h.defaultSort[defaultSortKey("", "")]
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultSortKey(schema, name string) string {
|
||||||
|
return fmt.Sprintf("%s.%s", schema, name)
|
||||||
|
}
|
||||||
|
|
||||||
// handlePanic is a helper function to handle panics with stack traces
|
// handlePanic is a helper function to handle panics with stack traces
|
||||||
func (h *Handler) handlePanic(w common.ResponseWriter, method string, err interface{}) {
|
func (h *Handler) handlePanic(w common.ResponseWriter, method string, err interface{}) {
|
||||||
stack := debug.Stack()
|
stack := debug.Stack()
|
||||||
@@ -645,6 +673,11 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
|||||||
query = query.Where(fmt.Sprintf("%s.%s = ?", common.QuoteIdent(tableAlias), common.QuoteIdent(pkName)), id)
|
query = query.Where(fmt.Sprintf("%s.%s = ?", common.QuoteIdent(tableAlias), common.QuoteIdent(pkName)), id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fall back to the configured default sort when the request didn't specify one
|
||||||
|
if len(options.Sort) == 0 {
|
||||||
|
options.Sort = common.ResolveSortColumns(h.getDefaultSort(schema, entity), reflection.GetPrimaryKeyName(model))
|
||||||
|
}
|
||||||
|
|
||||||
// Apply sorting
|
// Apply sorting
|
||||||
tableAlias := reflection.ExtractTableNameOnly(tableName)
|
tableAlias := reflection.ExtractTableNameOnly(tableName)
|
||||||
for _, sort := range options.Sort {
|
for _, sort := range options.Sort {
|
||||||
|
|||||||
Reference in New Issue
Block a user