• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pace / bricks / 13116624682

03 Feb 2025 03:13PM UTC coverage: 56.662% (-0.2%) from 56.856%
13116624682

push

github

web-flow
Merge pull request #382 from pace/bun-poc

Add Bun backend

144 of 189 new or added lines in 8 files covered. (76.19%)

48 existing lines in 4 files now uncovered.

5337 of 9419 relevant lines covered (56.66%)

21.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

2.07
/http/jsonapi/runtime/standard_params.go
1
// Copyright © 2020 by PACE Telematics GmbH. All rights reserved.
2

3
package runtime
4

5
import (
6
        "fmt"
7
        "net/http"
8
        "strconv"
9
        "strings"
10

11
        "github.com/caarlos0/env/v10"
12
        "github.com/uptrace/bun"
13

14
        "github.com/pace/bricks/maintenance/log"
15
)
16

17
type config struct {
18
        MaxPageSize     int `env:"MAX_PAGE_SIZE" envDefault:"100"`
19
        MinPageSize     int `env:"MIN_PAGE_SIZE" envDefault:"1"`
20
        DefaultPageSize int `env:"DEFAULT_PAGE_SIZE" envDefault:"50"`
21
}
22

23
var cfg config
24

25
func init() {
1✔
26
        err := env.Parse(&cfg)
1✔
27
        if err != nil {
1✔
28
                log.Fatalf("Failed to parse jsonapi params from environment: %v", err)
×
29
        }
×
30
}
31

32
// ValueSanitizer should sanitize query parameter values,
33
// the implementation should validate the value and transform it to the right type.
34
type ValueSanitizer interface {
35
        // SanitizeValue should sanitize a value, that should be in the column fieldName
36
        SanitizeValue(fieldName string, value string) (interface{}, error)
37
}
38

39
// ColumnMapper maps the name of a filter or sorting parameter to a database column name
40
type ColumnMapper interface {
41
        // Map maps the value, this function decides if the value is allowed and translates it to a database column name,
42
        // the function returns the database column name and a bool that indicates that the value is allowed and mapped
43
        Map(value string) (string, bool)
44
}
45

46
// MapMapper is a very easy ColumnMapper implementation based on a map which contains all allowed values
47
// and maps them with a map
48
type MapMapper struct {
49
        mapping map[string]string
50
}
51

52
// NewMapMapper returns a MapMapper for a specific map
53
func NewMapMapper(mapping map[string]string) *MapMapper {
×
54
        return &MapMapper{mapping: mapping}
×
55
}
×
56

57
// Map returns the mapped value and if it is valid based on a map
58
func (m *MapMapper) Map(value string) (string, bool) {
×
59
        val, isValid := m.mapping[value]
×
60
        return val, isValid
×
61
}
×
62

63
// UrlQueryParameters contains all information that is needed for pagination, sorting and filtering.
64
// It is not depending on orm.Query
65
type UrlQueryParameters struct {
66
        HasPagination bool
67
        PageNr        int
68
        PageSize      int
69
        Order         []string
70
        Filter        map[string][]any
71
}
72

73
// ReadURLQueryParameters reads sorting, filter and pagination from requests and return a UrlQueryParameters object,
74
// even if any errors occur. The returned error combines all errors of pagination, filter and sorting.
75
func ReadURLQueryParameters(r *http.Request, mapper ColumnMapper, sanitizer ValueSanitizer) (*UrlQueryParameters, error) {
×
76
        result := &UrlQueryParameters{}
×
77
        var errs []error
×
78
        if err := result.readPagination(r); err != nil {
×
79
                errs = append(errs, err)
×
80
        }
×
81
        if err := result.readSorting(r, mapper); err != nil {
×
82
                errs = append(errs, err)
×
83
        }
×
84
        if err := result.readFilter(r, mapper, sanitizer); err != nil {
×
85
                errs = append(errs, err)
×
86
        }
×
87
        if len(errs) == 0 {
×
88
                return result, nil
×
89
        }
×
90
        if len(errs) == 1 {
×
91
                return result, errs[0]
×
92
        }
×
93
        var errAggregate []string
×
94
        for _, err := range errs {
×
95
                errAggregate = append(errAggregate, err.Error())
×
96
        }
×
97
        return result, fmt.Errorf("reading URL Query Parameters cased multiple errors: %v", strings.Join(errAggregate, ","))
×
98
}
99

100
// AddToQuery adds filter, sorting and pagination to a query.
NEW
101
func (u *UrlQueryParameters) AddToQuery(query *bun.SelectQuery) *bun.SelectQuery {
×
102
        if u.HasPagination {
×
103
                query.Offset(u.PageSize * u.PageNr).Limit(u.PageSize)
×
104
        }
×
105

106
        for name, filterValues := range u.Filter {
×
107
                if len(filterValues) == 0 {
×
108
                        continue
×
109
                }
110

111
                if len(filterValues) == 1 {
×
112
                        query.Where(name+" = ?", filterValues[0])
×
113
                        continue
×
114
                }
115

NEW
116
                query.Where(name+" IN (?)", bun.In(filterValues))
×
117
        }
118

119
        for _, val := range u.Order {
×
120
                query.Order(val)
×
121
        }
×
122

UNCOV
123
        return query
×
124
}
125

126
func (u *UrlQueryParameters) readPagination(r *http.Request) error {
×
127
        pageStr := r.URL.Query().Get("page[number]")
×
128
        sizeStr := r.URL.Query().Get("page[size]")
×
129
        if pageStr == "" {
×
130
                u.HasPagination = false
×
131
                return nil
×
132
        }
×
133
        u.HasPagination = true
×
134
        pageNr, err := strconv.Atoi(pageStr)
×
135
        if err != nil {
×
136
                return err
×
137
        }
×
138
        var pageSize int
×
139
        if sizeStr != "" {
×
140
                pageSize, err = strconv.Atoi(sizeStr)
×
141
                if err != nil {
×
142
                        return err
×
143
                }
×
144
        } else {
×
145
                pageSize = cfg.DefaultPageSize
×
146
        }
×
147
        if (pageSize < cfg.MinPageSize) || (pageSize > cfg.MaxPageSize) {
×
148
                return fmt.Errorf("invalid pagesize not between min. and max. value, min: %d, max: %d", cfg.MinPageSize, cfg.MaxPageSize)
×
149
        }
×
150
        u.PageNr = pageNr
×
151
        u.PageSize = pageSize
×
152
        return nil
×
153
}
154

155
func (u *UrlQueryParameters) readSorting(r *http.Request, mapper ColumnMapper) error {
×
156
        sort := r.URL.Query().Get("sort")
×
157
        if sort == "" {
×
158
                return nil
×
159
        }
×
160
        sorting := strings.Split(sort, ",")
×
161

×
162
        var order string
×
163
        var resultedOrders []string
×
164
        var errSortingWithReason []string
×
165
        for _, val := range sorting {
×
166
                if val == "" {
×
167
                        continue
×
168
                }
169
                order = " ASC"
×
170
                if strings.HasPrefix(val, "-") {
×
171
                        order = " DESC"
×
172
                }
×
173
                val = strings.TrimPrefix(val, "-")
×
174

×
175
                key, isValid := mapper.Map(val)
×
176
                if !isValid {
×
177
                        errSortingWithReason = append(errSortingWithReason, val)
×
178
                        continue
×
179
                }
180
                resultedOrders = append(resultedOrders, key+order)
×
181
        }
182
        u.Order = resultedOrders
×
183
        if len(errSortingWithReason) > 0 {
×
184
                return fmt.Errorf("at least one sorting parameter is not valid: %q", strings.Join(errSortingWithReason, ","))
×
185
        }
×
186
        return nil
×
187
}
188

189
func (u *UrlQueryParameters) readFilter(r *http.Request, mapper ColumnMapper, sanitizer ValueSanitizer) error {
×
190
        filter := make(map[string][]interface{})
×
191
        var invalidFilter []string
×
192
        for queryName, queryValues := range r.URL.Query() {
×
193
                if !(strings.HasPrefix(queryName, "filter[") && strings.HasSuffix(queryName, "]")) {
×
194
                        continue
×
195
                }
196
                key, isValid := getFilterKey(queryName, mapper)
×
197
                if !isValid {
×
198
                        invalidFilter = append(invalidFilter, key)
×
199
                        continue
×
200
                }
201
                filterValues, isValid := getFilterValues(key, queryValues, sanitizer)
×
202
                if !isValid {
×
203
                        invalidFilter = append(invalidFilter, key)
×
204
                        continue
×
205
                }
206
                filter[key] = filterValues
×
207
        }
208
        u.Filter = filter
×
209
        if len(invalidFilter) != 0 {
×
210
                return fmt.Errorf("at least one filter parameter is not valid: %q", strings.Join(invalidFilter, ","))
×
211
        }
×
212
        return nil
×
213
}
214

215
func getFilterKey(queryName string, modelMapping ColumnMapper) (string, bool) {
×
216
        field := strings.TrimPrefix(queryName, "filter[")
×
217
        field = strings.TrimSuffix(field, "]")
×
218
        mapped, isValid := modelMapping.Map(field)
×
219
        if !isValid {
×
220
                return field, false
×
221
        }
×
222
        return mapped, true
×
223
}
224

225
func getFilterValues(fieldName string, queryValues []string, sanitizer ValueSanitizer) ([]interface{}, bool) {
×
226
        var filterValues []interface{}
×
227
        for _, value := range queryValues {
×
228
                separatedValues := strings.Split(value, ",")
×
229
                for _, separatedValue := range separatedValues {
×
230
                        sanitized, err := sanitizer.SanitizeValue(fieldName, separatedValue)
×
231
                        if err != nil {
×
232
                                return nil, false
×
233
                        }
×
234
                        filterValues = append(filterValues, sanitized)
×
235
                }
236
        }
237
        return filterValues, true
×
238
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc