mirror of
https://github.com/niusmallnan/steve.git
synced 2025-07-19 08:56:41 +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 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.
|
// The order is represented by prefixing the sort key by '-', e.g. sort=-metadata.name.
|
||||||
type Sort struct {
|
type Sort struct {
|
||||||
field []string
|
primaryField []string
|
||||||
order SortOrder
|
secondaryField []string
|
||||||
|
primaryOrder SortOrder
|
||||||
|
secondaryOrder SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the sort parameters as a query string.
|
// String returns the sort parameters as a query string.
|
||||||
func (s Sort) String() string {
|
func (s Sort) String() string {
|
||||||
field := strings.Join(s.field, ".")
|
field := ""
|
||||||
if s.order == DESC {
|
if s.primaryOrder == DESC {
|
||||||
field = "-" + field
|
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
|
return field
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,13 +117,27 @@ func ParseQuery(apiOp *types.APIRequest) *ListOptions {
|
|||||||
return fieldI < fieldJ
|
return fieldI < fieldJ
|
||||||
})
|
})
|
||||||
sortOpts := Sort{}
|
sortOpts := Sort{}
|
||||||
sortKey := q.Get(sortParam)
|
sortKeys := q.Get(sortParam)
|
||||||
if sortKey != "" && sortKey[0] == '-' {
|
if sortKeys != "" {
|
||||||
sortOpts.order = DESC
|
sortParts := strings.SplitN(sortKeys, ",", 2)
|
||||||
sortKey = sortKey[1:]
|
primaryField := sortParts[0]
|
||||||
}
|
if primaryField != "" && primaryField[0] == '-' {
|
||||||
if sortKey != "" {
|
sortOpts.primaryOrder = DESC
|
||||||
sortOpts.field = strings.Split(sortKey, ".")
|
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, ".")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
pagination := Pagination{}
|
pagination := Pagination{}
|
||||||
@ -233,16 +257,24 @@ func matchesAll(obj map[string]interface{}, filters []Filter) bool {
|
|||||||
|
|
||||||
// SortList sorts the slice by the provided sort criteria.
|
// SortList sorts the slice by the provided sort criteria.
|
||||||
func SortList(list []unstructured.Unstructured, s Sort) []unstructured.Unstructured {
|
func SortList(list []unstructured.Unstructured, s Sort) []unstructured.Unstructured {
|
||||||
if len(s.field) == 0 {
|
if len(s.primaryField) == 0 {
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
sort.Slice(list, func(i, j int) bool {
|
sort.Slice(list, func(i, j int) bool {
|
||||||
iField := convert.ToString(data.GetValueN(list[i].Object, s.field...))
|
leftPrime := convert.ToString(data.GetValueN(list[i].Object, s.primaryField...))
|
||||||
jField := convert.ToString(data.GetValueN(list[j].Object, s.field...))
|
rightPrime := convert.ToString(data.GetValueN(list[j].Object, s.primaryField...))
|
||||||
if s.order == ASC {
|
if leftPrime == rightPrime && len(s.secondaryField) > 0 {
|
||||||
return iField < jField
|
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 rightSecond < leftSecond
|
||||||
}
|
}
|
||||||
return jField < iField
|
if s.primaryOrder == ASC {
|
||||||
|
return leftPrime < rightPrime
|
||||||
|
}
|
||||||
|
return rightPrime < leftPrime
|
||||||
})
|
})
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
@ -842,7 +842,7 @@ func TestSortList(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
sort: Sort{
|
sort: Sort{
|
||||||
field: []string{"metadata", "name"},
|
primaryField: []string{"metadata", "name"},
|
||||||
},
|
},
|
||||||
want: []unstructured.Unstructured{
|
want: []unstructured.Unstructured{
|
||||||
{
|
{
|
||||||
@ -918,8 +918,8 @@ func TestSortList(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
sort: Sort{
|
sort: Sort{
|
||||||
field: []string{"metadata", "name"},
|
primaryField: []string{"metadata", "name"},
|
||||||
order: DESC,
|
primaryOrder: DESC,
|
||||||
},
|
},
|
||||||
want: []unstructured.Unstructured{
|
want: []unstructured.Unstructured{
|
||||||
{
|
{
|
||||||
@ -995,7 +995,7 @@ func TestSortList(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
sort: Sort{
|
sort: Sort{
|
||||||
field: []string{"data", "productType"},
|
primaryField: []string{"data", "productType"},
|
||||||
},
|
},
|
||||||
want: []unstructured.Unstructured{
|
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 {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
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",
|
name: "multi-partition sort=metadata.name",
|
||||||
apiOps: []*types.APIRequest{
|
apiOps: []*types.APIRequest{
|
||||||
|
Loading…
Reference in New Issue
Block a user