mirror of
https://github.com/niusmallnan/steve.git
synced 2025-07-19 17:07:10 +00:00
Add secondary sort parameter
Extend the sorting functionality in the partition store to support primary and secondary sorting criteria. Sorting parameters are specified by a single 'sort' query and comma separated. Example: Sort by name and creation time: GET /v1/secrets?sort=metadata.name,metadata.creationTimestamp Reverse sort by name, normal sort by creation time: GET /v1/secrets?sort=-metadata.name,metadata.creationTimestamp Normal sort by name, reverse sort by creation time: GET /v1/secrets?sort=metadata.name,-metadata.creationTimestamp
This commit is contained in:
parent
52e189b1ff
commit
b151f25581
@ -62,16 +62,26 @@ const (
|
||||
// The subfield is internally represented as a slice, e.g. [metadata, name].
|
||||
// The order is represented by prefixing the sort key by '-', e.g. sort=-metadata.name.
|
||||
type Sort struct {
|
||||
field []string
|
||||
order SortOrder
|
||||
primaryField []string
|
||||
secondaryField []string
|
||||
primaryOrder SortOrder
|
||||
secondaryOrder SortOrder
|
||||
}
|
||||
|
||||
// String returns the sort parameters as a query string.
|
||||
func (s Sort) String() string {
|
||||
field := strings.Join(s.field, ".")
|
||||
if s.order == DESC {
|
||||
field := ""
|
||||
if s.primaryOrder == DESC {
|
||||
field = "-" + field
|
||||
}
|
||||
field += strings.Join(s.primaryField, ".")
|
||||
if len(s.secondaryField) > 0 {
|
||||
field += ","
|
||||
if s.secondaryOrder == DESC {
|
||||
field += "-"
|
||||
}
|
||||
field += strings.Join(s.secondaryField, ".")
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
@ -107,13 +117,27 @@ func ParseQuery(apiOp *types.APIRequest) *ListOptions {
|
||||
return fieldI < fieldJ
|
||||
})
|
||||
sortOpts := Sort{}
|
||||
sortKey := q.Get(sortParam)
|
||||
if sortKey != "" && sortKey[0] == '-' {
|
||||
sortOpts.order = DESC
|
||||
sortKey = sortKey[1:]
|
||||
sortKeys := q.Get(sortParam)
|
||||
if sortKeys != "" {
|
||||
sortParts := strings.SplitN(sortKeys, ",", 2)
|
||||
primaryField := sortParts[0]
|
||||
if primaryField != "" && primaryField[0] == '-' {
|
||||
sortOpts.primaryOrder = DESC
|
||||
primaryField = primaryField[1:]
|
||||
}
|
||||
if primaryField != "" {
|
||||
sortOpts.primaryField = strings.Split(primaryField, ".")
|
||||
}
|
||||
if len(sortParts) > 1 {
|
||||
secondaryField := sortParts[1]
|
||||
if secondaryField != "" && secondaryField[0] == '-' {
|
||||
sortOpts.secondaryOrder = DESC
|
||||
secondaryField = secondaryField[1:]
|
||||
}
|
||||
if secondaryField != "" {
|
||||
sortOpts.secondaryField = strings.Split(secondaryField, ".")
|
||||
}
|
||||
}
|
||||
if sortKey != "" {
|
||||
sortOpts.field = strings.Split(sortKey, ".")
|
||||
}
|
||||
var err error
|
||||
pagination := Pagination{}
|
||||
@ -233,16 +257,24 @@ func matchesAll(obj map[string]interface{}, filters []Filter) bool {
|
||||
|
||||
// SortList sorts the slice by the provided sort criteria.
|
||||
func SortList(list []unstructured.Unstructured, s Sort) []unstructured.Unstructured {
|
||||
if len(s.field) == 0 {
|
||||
if len(s.primaryField) == 0 {
|
||||
return list
|
||||
}
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
iField := convert.ToString(data.GetValueN(list[i].Object, s.field...))
|
||||
jField := convert.ToString(data.GetValueN(list[j].Object, s.field...))
|
||||
if s.order == ASC {
|
||||
return iField < jField
|
||||
leftPrime := convert.ToString(data.GetValueN(list[i].Object, s.primaryField...))
|
||||
rightPrime := convert.ToString(data.GetValueN(list[j].Object, s.primaryField...))
|
||||
if leftPrime == rightPrime && len(s.secondaryField) > 0 {
|
||||
leftSecond := convert.ToString(data.GetValueN(list[i].Object, s.secondaryField...))
|
||||
rightSecond := convert.ToString(data.GetValueN(list[j].Object, s.secondaryField...))
|
||||
if s.secondaryOrder == ASC {
|
||||
return leftSecond < rightSecond
|
||||
}
|
||||
return jField < iField
|
||||
return rightSecond < leftSecond
|
||||
}
|
||||
if s.primaryOrder == ASC {
|
||||
return leftPrime < rightPrime
|
||||
}
|
||||
return rightPrime < leftPrime
|
||||
})
|
||||
return list
|
||||
}
|
||||
|
@ -842,7 +842,7 @@ func TestSortList(t *testing.T) {
|
||||
},
|
||||
},
|
||||
sort: Sort{
|
||||
field: []string{"metadata", "name"},
|
||||
primaryField: []string{"metadata", "name"},
|
||||
},
|
||||
want: []unstructured.Unstructured{
|
||||
{
|
||||
@ -918,8 +918,8 @@ func TestSortList(t *testing.T) {
|
||||
},
|
||||
},
|
||||
sort: Sort{
|
||||
field: []string{"metadata", "name"},
|
||||
order: DESC,
|
||||
primaryField: []string{"metadata", "name"},
|
||||
primaryOrder: DESC,
|
||||
},
|
||||
want: []unstructured.Unstructured{
|
||||
{
|
||||
@ -995,7 +995,7 @@ func TestSortList(t *testing.T) {
|
||||
},
|
||||
},
|
||||
sort: Sort{
|
||||
field: []string{"data", "productType"},
|
||||
primaryField: []string{"data", "productType"},
|
||||
},
|
||||
want: []unstructured.Unstructured{
|
||||
{
|
||||
@ -1107,6 +1107,318 @@ func TestSortList(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "primary sort ascending, secondary sort ascending",
|
||||
objects: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "fuji",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "honeycrisp",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "granny-smith",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "green",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sort: Sort{
|
||||
primaryField: []string{"data", "color"},
|
||||
secondaryField: []string{"metadata", "name"},
|
||||
},
|
||||
want: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "granny-smith",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "green",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "fuji",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "honeycrisp",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "primary sort ascending, secondary sort descending",
|
||||
objects: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "fuji",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "honeycrisp",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "granny-smith",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "green",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sort: Sort{
|
||||
primaryField: []string{"data", "color"},
|
||||
secondaryField: []string{"metadata", "name"},
|
||||
secondaryOrder: DESC,
|
||||
},
|
||||
want: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "granny-smith",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "green",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "honeycrisp",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "fuji",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "primary sort descending, secondary sort ascending",
|
||||
objects: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "fuji",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "honeycrisp",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "granny-smith",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "green",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sort: Sort{
|
||||
primaryField: []string{"data", "color"},
|
||||
primaryOrder: DESC,
|
||||
secondaryField: []string{"metadata", "name"},
|
||||
},
|
||||
want: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "fuji",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "honeycrisp",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "granny-smith",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "green",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "primary sort descending, secondary sort descending",
|
||||
objects: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "fuji",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "honeycrisp",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "granny-smith",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "green",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sort: Sort{
|
||||
primaryField: []string{"data", "color"},
|
||||
primaryOrder: DESC,
|
||||
secondaryField: []string{"metadata", "name"},
|
||||
secondaryOrder: DESC,
|
||||
},
|
||||
want: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "honeycrisp",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "fuji",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "granny-smith",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "green",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
@ -413,6 +413,117 @@ func TestList(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sorting with secondary sort",
|
||||
apiOps: []*types.APIRequest{
|
||||
newRequest("sort=data.color,metadata.name,", "user1"),
|
||||
},
|
||||
access: []map[string]string{
|
||||
{
|
||||
"user1": "roleA",
|
||||
},
|
||||
},
|
||||
partitions: map[string][]Partition{
|
||||
"user1": {
|
||||
mockPartition{
|
||||
name: "all",
|
||||
},
|
||||
},
|
||||
},
|
||||
objects: map[string]*unstructured.UnstructuredList{
|
||||
"all": {
|
||||
Items: []unstructured.Unstructured{
|
||||
newApple("fuji").Unstructured,
|
||||
newApple("honeycrisp").Unstructured,
|
||||
newApple("granny-smith").Unstructured,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []types.APIObjectList{
|
||||
{
|
||||
Count: 3,
|
||||
Objects: []types.APIObject{
|
||||
newApple("granny-smith").toObj(),
|
||||
newApple("fuji").toObj(),
|
||||
newApple("honeycrisp").toObj(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sorting with missing primary sort is unsorted",
|
||||
apiOps: []*types.APIRequest{
|
||||
newRequest("sort=,metadata.name", "user1"),
|
||||
},
|
||||
access: []map[string]string{
|
||||
{
|
||||
"user1": "roleA",
|
||||
},
|
||||
},
|
||||
partitions: map[string][]Partition{
|
||||
"user1": {
|
||||
mockPartition{
|
||||
name: "all",
|
||||
},
|
||||
},
|
||||
},
|
||||
objects: map[string]*unstructured.UnstructuredList{
|
||||
"all": {
|
||||
Items: []unstructured.Unstructured{
|
||||
newApple("fuji").Unstructured,
|
||||
newApple("honeycrisp").Unstructured,
|
||||
newApple("granny-smith").Unstructured,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []types.APIObjectList{
|
||||
{
|
||||
Count: 3,
|
||||
Objects: []types.APIObject{
|
||||
newApple("fuji").toObj(),
|
||||
newApple("honeycrisp").toObj(),
|
||||
newApple("granny-smith").toObj(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sorting with missing secondary sort is single-column sorted",
|
||||
apiOps: []*types.APIRequest{
|
||||
newRequest("sort=metadata.name,", "user1"),
|
||||
},
|
||||
access: []map[string]string{
|
||||
{
|
||||
"user1": "roleA",
|
||||
},
|
||||
},
|
||||
partitions: map[string][]Partition{
|
||||
"user1": {
|
||||
mockPartition{
|
||||
name: "all",
|
||||
},
|
||||
},
|
||||
},
|
||||
objects: map[string]*unstructured.UnstructuredList{
|
||||
"all": {
|
||||
Items: []unstructured.Unstructured{
|
||||
newApple("fuji").Unstructured,
|
||||
newApple("honeycrisp").Unstructured,
|
||||
newApple("granny-smith").Unstructured,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []types.APIObjectList{
|
||||
{
|
||||
Count: 3,
|
||||
Objects: []types.APIObject{
|
||||
newApple("fuji").toObj(),
|
||||
newApple("granny-smith").toObj(),
|
||||
newApple("honeycrisp").toObj(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multi-partition sort=metadata.name",
|
||||
apiOps: []*types.APIRequest{
|
||||
|
Loading…
Reference in New Issue
Block a user