mirror of
https://github.com/oxynote/oxynote.git
synced 2026-10-17 19:09:19 +02:00
216 lines
5.4 KiB
Go
216 lines
5.4 KiB
Go
package httpserver
|
|
|
|
import (
|
|
"net/http"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/oxynote/oxynote/server/core/pkg/errutil"
|
|
)
|
|
|
|
const (
|
|
// _filterPrefix is a prefix that is used to group HTTP query
|
|
// filters together.
|
|
_filterPrefix = "filter-"
|
|
|
|
// _sortPrefix is a prefix that is used to group HTTP query
|
|
// sort keys together.
|
|
_sortPrefix = "sort-"
|
|
|
|
// _sortKeyParts specifies the number of dash-separated segments
|
|
// a raw sort key consists of (prefix, priority, key).
|
|
_sortKeyParts = 3
|
|
|
|
// _sortAsc is the ascending sort direction value.
|
|
_sortAsc = "asc"
|
|
|
|
// _sortDesc is the descending sort direction value.
|
|
_sortDesc = "desc"
|
|
)
|
|
|
|
var (
|
|
// ErrInvalidFilterKey is returned when filter key is
|
|
// determined to be invalid.
|
|
ErrInvalidFilterKey = errutil.New(http.StatusBadRequest, "request.invalid_filter_key", "invalid filter key")
|
|
|
|
// ErrInvalidFilterValue is returned when filter value is
|
|
// determined to be invalid.
|
|
ErrInvalidFilterValue = errutil.New(http.StatusBadRequest, "request.invalid_filter_value", "invalid filter value")
|
|
|
|
// ErrInvalidSortKey is returned when sort key is
|
|
// determined to be invalid.
|
|
ErrInvalidSortKey = errutil.New(http.StatusBadRequest, "request.invalid_sort_key", "invalid sort key")
|
|
)
|
|
|
|
// Query is used to filter and retrieve bulk data from
|
|
// data stores.
|
|
type Query struct {
|
|
// Limit specifies the total amount of data elements per page.
|
|
Limit uint64 `json:"limit" schema:"limit"`
|
|
|
|
// Page specifies data batch number.
|
|
Page uint64 `json:"page" schema:"page"`
|
|
|
|
// Filters specifies a map of fields and their expected values (can
|
|
// be partial).
|
|
// Filter's key must start with the "filter-" prefix in the request's
|
|
// query parameters to be put in this map.
|
|
Filters map[string]string `json:"filters,omitempty" schema:"-"`
|
|
|
|
// SortKeys specifies a slice of fields that should be used
|
|
// for sorting.
|
|
// Sorting key must start with the "sort-" prefix in the request's
|
|
// query parameters to be put in this slice.
|
|
// NOTE: the order of keys is important.
|
|
SortKeys []SortKey `json:"sortKeys,omitempty" schema:"-"`
|
|
|
|
// Omissions specifies a list of keys that should be
|
|
// omitted from the response.
|
|
Omissions map[string]struct{} `json:"omissions,omitempty" schema:"-"`
|
|
}
|
|
|
|
// SortKey is used to sort data by a specific key.
|
|
type SortKey struct {
|
|
// Key specifies the name of the field by which elements should
|
|
// be sorted.
|
|
Key string `json:"key"`
|
|
|
|
// Asc specifies whether ascending sorting order should be used.
|
|
Asc bool `json:"asc"`
|
|
}
|
|
|
|
// Validate checks whether query data is valid or not.
|
|
// Since filters and sort keys may be dynamic, they should be validated
|
|
// on the outside.
|
|
func (q Query) Validate() error {
|
|
if q.Limit < 1 {
|
|
return errutil.New(http.StatusBadRequest, "request.invalid_query_limit", "invalid limit")
|
|
}
|
|
|
|
if q.Page < 1 {
|
|
return errutil.New(http.StatusBadRequest, "request.invalid_query_page", "invalid page")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ParseQuery parses request's query parameters into query structure.
|
|
//
|
|
//nolint:gocognit // sorting key parsing requires additional checks.
|
|
func ParseQuery(r *http.Request) (Query, error) {
|
|
var qr Query
|
|
|
|
if err := DecodeForm(r, &qr); err != nil {
|
|
return Query{}, err
|
|
}
|
|
|
|
// to avoid duplicates, we first need to put sort keys into a map
|
|
var (
|
|
sortKeys map[uint64]SortKey
|
|
sortPriority []uint64 // to maintain proper order
|
|
)
|
|
|
|
for k := range r.Form {
|
|
switch {
|
|
case strings.HasPrefix(k, _filterPrefix):
|
|
nk := strings.TrimPrefix(k, _filterPrefix)
|
|
if nk == "" {
|
|
continue
|
|
}
|
|
|
|
v := r.Form.Get(k)
|
|
if v == "" {
|
|
return Query{}, ErrInvalidFilterValue
|
|
}
|
|
|
|
if qr.Filters == nil {
|
|
qr.Filters = make(map[string]string)
|
|
}
|
|
|
|
qr.Filters[nk] = v
|
|
case strings.HasPrefix(k, _sortPrefix):
|
|
// some sort keys might contain more than three
|
|
// dashes, but we care only about the first two
|
|
ss := strings.SplitN(k, "-", _sortKeyParts)
|
|
if len(ss) != _sortKeyParts {
|
|
continue
|
|
}
|
|
|
|
priority, err := strconv.ParseUint(ss[1], 10, 64)
|
|
if err != nil {
|
|
return Query{}, errutil.New(
|
|
http.StatusBadRequest,
|
|
"request.invalid_sort_index",
|
|
"invalid sort index",
|
|
)
|
|
}
|
|
|
|
var asc bool
|
|
|
|
switch r.Form.Get(k) {
|
|
case _sortAsc:
|
|
asc = true
|
|
case _sortDesc:
|
|
// asc is false already
|
|
default:
|
|
return Query{}, errutil.New(
|
|
http.StatusBadRequest,
|
|
"request.invalid_sort_direction",
|
|
"invalid sort direction",
|
|
)
|
|
}
|
|
|
|
// check for duplicates
|
|
for ski, sk := range sortKeys {
|
|
switch {
|
|
case ski == priority:
|
|
return Query{}, errutil.New(
|
|
http.StatusBadRequest,
|
|
"request.duplicate_sort_priority",
|
|
"duplicate sort priority",
|
|
)
|
|
case sk.Key == ss[2]:
|
|
return Query{}, errutil.New(
|
|
http.StatusBadRequest,
|
|
"request.duplicate_sort_key",
|
|
"duplicate sort key",
|
|
)
|
|
}
|
|
}
|
|
|
|
if sortKeys == nil {
|
|
sortKeys = make(map[uint64]SortKey)
|
|
}
|
|
|
|
sortKeys[priority] = SortKey{
|
|
Key: ss[2],
|
|
Asc: asc,
|
|
}
|
|
|
|
sortPriority = append(sortPriority, priority)
|
|
case k == "omissions":
|
|
vv := r.Form[k]
|
|
for i := range vv {
|
|
if qr.Omissions == nil {
|
|
qr.Omissions = make(map[string]struct{}, len(vv))
|
|
}
|
|
|
|
qr.Omissions[vv[i]] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
slices.Sort(sortPriority)
|
|
|
|
if sortKeys != nil {
|
|
qr.SortKeys = make([]SortKey, 0, len(sortKeys))
|
|
}
|
|
|
|
for _, priority := range sortPriority {
|
|
qr.SortKeys = append(qr.SortKeys, sortKeys[priority])
|
|
}
|
|
|
|
return qr, nil
|
|
}
|