fix: make sorting of strings case insensitive

This commit is contained in:
Elias Schneider
2026-08-28 21:49:20 +02:00
parent 2abdf44320
commit 3657012f93
7 changed files with 84 additions and 17 deletions

View File

@@ -9,8 +9,8 @@ import (
type API struct {
model.Base
Name string `sortable:"true"`
Audience string `sortable:"true"`
Name string `sortable:"case-insensitive"`
Audience string `sortable:"case-insensitive"`
UpdatedAt *datatype.DateTime
AllowCIMDClients bool `gorm:"column:allow_cimd_clients"`

View File

@@ -9,7 +9,7 @@ import (
type ApiKey struct {
model.Base
Name string `sortable:"true"`
Name string `sortable:"case-insensitive"`
Key string
Description *string
ExpiresAt datatype.DateTime `sortable:"true"`

View File

@@ -46,7 +46,7 @@ const (
type OidcClient struct {
Base
Name string `sortable:"true"`
Name string `sortable:"case-insensitive"`
Description string
CallbackURLs datatype.StringList
LogoutCallbackURLs datatype.StringList

View File

@@ -14,12 +14,12 @@ import (
type User struct {
Base
Username string `sortable:"true"`
Email *string `sortable:"true"`
Username string `sortable:"case-insensitive"`
Email *string `sortable:"case-insensitive"`
EmailVerified bool `sortable:"true" filterable:"true"`
FirstName string `sortable:"true"`
LastName string `sortable:"true"`
DisplayName string `sortable:"true"`
FirstName string `sortable:"case-insensitive"`
LastName string `sortable:"case-insensitive"`
DisplayName string `sortable:"case-insensitive"`
IsAdmin bool `sortable:"true" filterable:"true"`
Locale *string
LdapID *string

View File

@@ -8,8 +8,8 @@ import (
type UserGroup struct {
Base
FriendlyName string `sortable:"true"`
Name string `sortable:"true"`
FriendlyName string `sortable:"case-insensitive"`
Name string `sortable:"case-insensitive"`
LdapID *string
UpdatedAt *datatype.DateTime
Users []User `gorm:"many2many:user_groups_users;"`

View File

@@ -29,9 +29,10 @@ type ListRequestOptions struct {
}
type FieldMeta struct {
ColumnName string
IsSortable bool
IsFilterable bool
ColumnName string
IsSortable bool
IsFilterable bool
IsCaseInsensitive bool
}
func ParseListRequestOptions(ctx *gin.Context) (listRequestOptions ListRequestOptions) {
@@ -155,6 +156,18 @@ func applySorting(sortColumn string, sortDirection string, query *gorm.DB, meta
}
sortDirection = NormalizeSortDirection(sortDirection)
if fieldMeta.IsCaseInsensitive {
expression := "LOWER(?)"
if sortDirection == "desc" {
expression += " DESC"
}
return query.Clauses(clause.OrderBy{
Expression: clause.Expr{
SQL: expression,
Vars: []any{clause.Column{Name: fieldMeta.ColumnName}},
},
})
}
query = query.Clauses(clause.OrderBy{
Columns: []clause.OrderByColumn{
@@ -191,10 +204,12 @@ func extractModelMetadata(model any) map[string]FieldMeta {
// Normal field: record metadata
name := field.Name
sortMode := field.Tag.Get("sortable")
meta[name] = FieldMeta{
ColumnName: CamelCaseToSnakeCase(name),
IsSortable: field.Tag.Get("sortable") == "true",
IsFilterable: field.Tag.Get("filterable") == "true",
ColumnName: CamelCaseToSnakeCase(name),
IsSortable: sortMode == "true" || sortMode == "case-insensitive",
IsFilterable: field.Tag.Get("filterable") == "true",
IsCaseInsensitive: sortMode == "case-insensitive",
}
}
}

View File

@@ -0,0 +1,52 @@
package utils
import (
"testing"
"github.com/libtnb/sqlite"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
func TestPaginateFilterAndSortSortsStringsCaseInsensitively(t *testing.T) {
type record struct {
ID uint
Name string `sortable:"case-insensitive"`
}
db, err := gorm.Open(sqlite.Open("file:"+CreateSha256Hash(t.Name())+"?mode=memory&cache=shared"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.Exec("CREATE TABLE records (id INTEGER PRIMARY KEY, name TEXT NOT NULL)").Error)
require.NoError(t, db.Create(&[]record{{Name: "alpha"}, {Name: "Charlie"}, {Name: "bravo"}}).Error)
for _, test := range []struct {
direction string
expected []string
}{
{direction: "asc", expected: []string{"alpha", "bravo", "Charlie"}},
{direction: "desc", expected: []string{"Charlie", "bravo", "alpha"}},
} {
t.Run(test.direction, func(t *testing.T) {
options := ListRequestOptions{}
options.Sort.Column = "name"
options.Sort.Direction = test.direction
var records []record
_, err := PaginateFilterAndSort(options, db.Model(&record{}), &records)
require.NoError(t, err)
require.Len(t, records, 3)
require.Equal(t, test.expected, []string{records[0].Name, records[1].Name, records[2].Name})
})
}
}
func TestExtractModelMetadataRequiresCaseInsensitiveSortMode(t *testing.T) {
type record struct {
Name string `sortable:"case-insensitive"`
Event string `sortable:"true"`
}
meta := extractModelMetadata(&[]record{})
require.True(t, meta["Name"].IsCaseInsensitive)
require.False(t, meta["Event"].IsCaseInsensitive)
}