mirror of
https://github.com/dstotijn/hetty
synced 2024-11-10 06:04:19 +00:00
Add search expression support to admin interface
This commit is contained in:
parent
8ab65fb55f
commit
194d727f4f
11 changed files with 469 additions and 19 deletions
|
@ -31,6 +31,7 @@ const FILTER = gql`
|
|||
query HttpRequestLogFilter {
|
||||
httpRequestLogFilter {
|
||||
onlyInScope
|
||||
searchExpression
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -39,6 +40,7 @@ const SET_FILTER = gql`
|
|||
mutation SetHttpRequestLogFilter($filter: HttpRequestLogFilterInput) {
|
||||
setHttpRequestLogFilter(filter: $filter) {
|
||||
onlyInScope
|
||||
searchExpression
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -75,14 +77,21 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||
|
||||
export interface SearchFilter {
|
||||
onlyInScope: boolean;
|
||||
searchExpression: string;
|
||||
}
|
||||
|
||||
function Search(): JSX.Element {
|
||||
const classes = useStyles();
|
||||
const theme = useTheme();
|
||||
|
||||
const [searchExpr, setSearchExpr] = useState("");
|
||||
const { loading: filterLoading, error: filterErr, data: filter } = useQuery(
|
||||
FILTER
|
||||
FILTER,
|
||||
{
|
||||
onCompleted: (data) => {
|
||||
setSearchExpr(data.httpRequestLogFilter.searchExpression || "");
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const [
|
||||
|
@ -111,6 +120,15 @@ function Search(): JSX.Element {
|
|||
const [filterOpen, setFilterOpen] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.SyntheticEvent) => {
|
||||
setFilterMutate({
|
||||
variables: {
|
||||
filter: {
|
||||
...withoutTypename(filter?.httpRequestLogFilter),
|
||||
searchExpression: searchExpr,
|
||||
},
|
||||
},
|
||||
});
|
||||
setFilterOpen(false);
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
|
@ -142,10 +160,9 @@ function Search(): JSX.Element {
|
|||
className={classes.iconButton}
|
||||
onClick={() => setFilterOpen(!filterOpen)}
|
||||
style={{
|
||||
color:
|
||||
filter?.httpRequestLogFilter !== null
|
||||
? theme.palette.secondary.main
|
||||
: "inherit",
|
||||
color: filter?.httpRequestLogFilter?.onlyInScope
|
||||
? theme.palette.secondary.main
|
||||
: "inherit",
|
||||
}}
|
||||
>
|
||||
{filterLoading || setFilterLoading ? (
|
||||
|
@ -161,6 +178,8 @@ function Search(): JSX.Element {
|
|||
<InputBase
|
||||
className={classes.input}
|
||||
placeholder="Search proxy logs…"
|
||||
value={searchExpr}
|
||||
onChange={(e) => setSearchExpr(e.target.value)}
|
||||
onFocus={() => setFilterOpen(true)}
|
||||
/>
|
||||
<Tooltip title="Search">
|
||||
|
|
|
@ -72,7 +72,8 @@ type ComplexityRoot struct {
|
|||
}
|
||||
|
||||
HTTPRequestLogFilter struct {
|
||||
OnlyInScope func(childComplexity int) int
|
||||
OnlyInScope func(childComplexity int) int
|
||||
SearchExpression func(childComplexity int) int
|
||||
}
|
||||
|
||||
HTTPResponseLog struct {
|
||||
|
@ -249,6 +250,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||
|
||||
return e.complexity.HTTPRequestLogFilter.OnlyInScope(childComplexity), true
|
||||
|
||||
case "HttpRequestLogFilter.searchExpression":
|
||||
if e.complexity.HTTPRequestLogFilter.SearchExpression == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.HTTPRequestLogFilter.SearchExpression(childComplexity), true
|
||||
|
||||
case "HttpResponseLog.body":
|
||||
if e.complexity.HTTPResponseLog.Body == nil {
|
||||
break
|
||||
|
@ -579,10 +587,12 @@ type ClearHTTPRequestLogResult {
|
|||
|
||||
input HttpRequestLogFilterInput {
|
||||
onlyInScope: Boolean
|
||||
searchExpression: String
|
||||
}
|
||||
|
||||
type HttpRequestLogFilter {
|
||||
onlyInScope: Boolean!
|
||||
searchExpression: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
|
@ -1239,6 +1249,38 @@ func (ec *executionContext) _HttpRequestLogFilter_onlyInScope(ctx context.Contex
|
|||
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _HttpRequestLogFilter_searchExpression(ctx context.Context, field graphql.CollectedField, obj *HTTPRequestLogFilter) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "HttpRequestLogFilter",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
}
|
||||
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.SearchExpression, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*string)
|
||||
fc.Result = res
|
||||
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _HttpResponseLog_requestId(ctx context.Context, field graphql.CollectedField, obj *HTTPResponseLog) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
|
@ -3288,6 +3330,14 @@ func (ec *executionContext) unmarshalInputHttpRequestLogFilterInput(ctx context.
|
|||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
case "searchExpression":
|
||||
var err error
|
||||
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("searchExpression"))
|
||||
it.SearchExpression, err = ec.unmarshalOString2ᚖstring(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3551,6 +3601,8 @@ func (ec *executionContext) _HttpRequestLogFilter(ctx context.Context, sel ast.S
|
|||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
case "searchExpression":
|
||||
out.Values[i] = ec._HttpRequestLogFilter_searchExpression(ctx, field, obj)
|
||||
default:
|
||||
panic("unknown field " + strconv.Quote(field.Name))
|
||||
}
|
||||
|
|
|
@ -38,11 +38,13 @@ type HTTPRequestLog struct {
|
|||
}
|
||||
|
||||
type HTTPRequestLogFilter struct {
|
||||
OnlyInScope bool `json:"onlyInScope"`
|
||||
OnlyInScope bool `json:"onlyInScope"`
|
||||
SearchExpression *string `json:"searchExpression"`
|
||||
}
|
||||
|
||||
type HTTPRequestLogFilterInput struct {
|
||||
OnlyInScope *bool `json:"onlyInScope"`
|
||||
OnlyInScope *bool `json:"onlyInScope"`
|
||||
SearchExpression *string `json:"searchExpression"`
|
||||
}
|
||||
|
||||
type HTTPResponseLog struct {
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/dstotijn/hetty/pkg/proj"
|
||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||
"github.com/dstotijn/hetty/pkg/scope"
|
||||
"github.com/dstotijn/hetty/pkg/search"
|
||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||
)
|
||||
|
||||
|
@ -263,15 +264,14 @@ func (r *mutationResolver) SetHTTPRequestLogFilter(
|
|||
ctx context.Context,
|
||||
input *HTTPRequestLogFilterInput,
|
||||
) (*HTTPRequestLogFilter, error) {
|
||||
filter := findRequestsFilterFromInput(input)
|
||||
filter, err := findRequestsFilterFromInput(input)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse request log filter: %v", err)
|
||||
}
|
||||
if err := r.RequestLogService.SetRequestLogFilter(ctx, filter); err != nil {
|
||||
return nil, fmt.Errorf("could not set request log filter: %v", err)
|
||||
}
|
||||
|
||||
empty := reqlog.FindRequestsFilter{}
|
||||
if filter == empty {
|
||||
return nil, nil
|
||||
}
|
||||
return findReqFilterToHTTPReqLogFilter(filter), nil
|
||||
}
|
||||
|
||||
|
@ -297,13 +297,21 @@ func scopeToScopeRules(rules []scope.Rule) []ScopeRule {
|
|||
return scopeRules
|
||||
}
|
||||
|
||||
func findRequestsFilterFromInput(input *HTTPRequestLogFilterInput) (filter reqlog.FindRequestsFilter) {
|
||||
func findRequestsFilterFromInput(input *HTTPRequestLogFilterInput) (filter reqlog.FindRequestsFilter, err error) {
|
||||
if input == nil {
|
||||
return
|
||||
}
|
||||
if input.OnlyInScope != nil {
|
||||
filter.OnlyInScope = *input.OnlyInScope
|
||||
}
|
||||
if input.SearchExpression != nil && *input.SearchExpression != "" {
|
||||
expr, err := search.ParseQuery(*input.SearchExpression)
|
||||
if err != nil {
|
||||
return reqlog.FindRequestsFilter{}, fmt.Errorf("could not parse search query: %v", err)
|
||||
}
|
||||
filter.RawSearchExpr = *input.SearchExpression
|
||||
filter.SearchExpr = expr
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -317,5 +325,9 @@ func findReqFilterToHTTPReqLogFilter(findReqFilter reqlog.FindRequestsFilter) *H
|
|||
OnlyInScope: findReqFilter.OnlyInScope,
|
||||
}
|
||||
|
||||
if findReqFilter.RawSearchExpr != "" {
|
||||
httpReqLogFilter.SearchExpression = &findReqFilter.RawSearchExpr
|
||||
}
|
||||
|
||||
return httpReqLogFilter
|
||||
}
|
||||
|
|
|
@ -64,10 +64,12 @@ type ClearHTTPRequestLogResult {
|
|||
|
||||
input HttpRequestLogFilterInput {
|
||||
onlyInScope: Boolean
|
||||
searchExpression: String
|
||||
}
|
||||
|
||||
type HttpRequestLogFilter {
|
||||
onlyInScope: Boolean!
|
||||
searchExpression: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
|
|
108
pkg/db/sqlite/search.go
Normal file
108
pkg/db/sqlite/search.go
Normal file
|
@ -0,0 +1,108 @@
|
|||
package sqlite
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/dstotijn/hetty/pkg/search"
|
||||
)
|
||||
|
||||
var stringLiteralMap = map[string]string{
|
||||
// http_requests
|
||||
"req.id": "req.id",
|
||||
"req.proto": "req.proto",
|
||||
"req.url": "req.url",
|
||||
"req.method": "req.method",
|
||||
"req.body": "req.body",
|
||||
"req.timestamp": "req.timestamp",
|
||||
// http_responses
|
||||
"res.id": "res.id",
|
||||
"res.proto": "res.proto",
|
||||
"res.statusCode": "res.status_code",
|
||||
"res.statusReason": "res.status_reason",
|
||||
"res.body": "res.body",
|
||||
"res.timestamp": "res.timestamp",
|
||||
// TODO: http_headers
|
||||
}
|
||||
|
||||
func parseSearchExpr(expr search.Expression) (sq.Sqlizer, error) {
|
||||
switch e := expr.(type) {
|
||||
case *search.PrefixExpression:
|
||||
return parsePrefixExpr(e)
|
||||
case *search.InfixExpression:
|
||||
return parseInfixExpr(e)
|
||||
default:
|
||||
return nil, fmt.Errorf("expression type (%v) not supported", expr)
|
||||
}
|
||||
}
|
||||
|
||||
func parsePrefixExpr(expr *search.PrefixExpression) (sq.Sqlizer, error) {
|
||||
switch expr.Operator {
|
||||
case search.TokOpNot:
|
||||
// TODO: Find a way to prefix an `sq.Sqlizer` with "NOT".
|
||||
return nil, errors.New("not implemented")
|
||||
default:
|
||||
return nil, errors.New("operator is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
func parseInfixExpr(expr *search.InfixExpression) (sq.Sqlizer, error) {
|
||||
switch expr.Operator {
|
||||
case search.TokOpAnd:
|
||||
left, err := parseSearchExpr(expr.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right, err := parseSearchExpr(expr.Right)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sq.And{left, right}, nil
|
||||
case search.TokOpOr:
|
||||
left, err := parseSearchExpr(expr.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right, err := parseSearchExpr(expr.Right)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sq.Or{left, right}, nil
|
||||
}
|
||||
|
||||
left, ok := expr.Left.(*search.StringLiteral)
|
||||
if !ok {
|
||||
return nil, errors.New("left operand must be a string literal")
|
||||
}
|
||||
right, ok := expr.Right.(*search.StringLiteral)
|
||||
if !ok {
|
||||
return nil, errors.New("right operand must be a string literal")
|
||||
}
|
||||
|
||||
mappedLeft, ok := stringLiteralMap[left.Value]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid string literal: %v", left)
|
||||
}
|
||||
|
||||
switch expr.Operator {
|
||||
case search.TokOpEq:
|
||||
return sq.Eq{mappedLeft: right.Value}, nil
|
||||
case search.TokOpNotEq:
|
||||
return sq.NotEq{mappedLeft: right.Value}, nil
|
||||
case search.TokOpGt:
|
||||
return sq.Gt{mappedLeft: right.Value}, nil
|
||||
case search.TokOpLt:
|
||||
return sq.Lt{mappedLeft: right.Value}, nil
|
||||
case search.TokOpGtEq:
|
||||
return sq.GtOrEq{mappedLeft: right.Value}, nil
|
||||
case search.TokOpLtEq:
|
||||
return sq.LtOrEq{mappedLeft: right.Value}, nil
|
||||
case search.TokOpRe:
|
||||
return sq.Expr(fmt.Sprintf("regexp(?, %v)", mappedLeft), right.Value), nil
|
||||
case search.TokOpNotRe:
|
||||
return sq.Expr(fmt.Sprintf("NOT regexp(?, %v)", mappedLeft), right.Value), nil
|
||||
default:
|
||||
return nil, errors.New("unsupported operator")
|
||||
}
|
||||
}
|
197
pkg/db/sqlite/search_test.go
Normal file
197
pkg/db/sqlite/search_test.go
Normal file
|
@ -0,0 +1,197 @@
|
|||
package sqlite
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
|
||||
"github.com/dstotijn/hetty/pkg/search"
|
||||
)
|
||||
|
||||
func TestParseSearchExpr(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
searchExpr search.Expression
|
||||
expectedSqlizer sq.Sqlizer
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "req.body = bar",
|
||||
searchExpr: &search.InfixExpression{
|
||||
Operator: search.TokOpEq,
|
||||
Left: &search.StringLiteral{Value: "req.body"},
|
||||
Right: &search.StringLiteral{Value: "bar"},
|
||||
},
|
||||
expectedSqlizer: sq.Eq{"req.body": "bar"},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "req.body != bar",
|
||||
searchExpr: &search.InfixExpression{
|
||||
Operator: search.TokOpNotEq,
|
||||
Left: &search.StringLiteral{Value: "req.body"},
|
||||
Right: &search.StringLiteral{Value: "bar"},
|
||||
},
|
||||
expectedSqlizer: sq.NotEq{"req.body": "bar"},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "req.body > bar",
|
||||
searchExpr: &search.InfixExpression{
|
||||
Operator: search.TokOpGt,
|
||||
Left: &search.StringLiteral{Value: "req.body"},
|
||||
Right: &search.StringLiteral{Value: "bar"},
|
||||
},
|
||||
expectedSqlizer: sq.Gt{"req.body": "bar"},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "req.body < bar",
|
||||
searchExpr: &search.InfixExpression{
|
||||
Operator: search.TokOpLt,
|
||||
Left: &search.StringLiteral{Value: "req.body"},
|
||||
Right: &search.StringLiteral{Value: "bar"},
|
||||
},
|
||||
expectedSqlizer: sq.Lt{"req.body": "bar"},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "req.body >= bar",
|
||||
searchExpr: &search.InfixExpression{
|
||||
Operator: search.TokOpGtEq,
|
||||
Left: &search.StringLiteral{Value: "req.body"},
|
||||
Right: &search.StringLiteral{Value: "bar"},
|
||||
},
|
||||
expectedSqlizer: sq.GtOrEq{"req.body": "bar"},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "req.body <= bar",
|
||||
searchExpr: &search.InfixExpression{
|
||||
Operator: search.TokOpLtEq,
|
||||
Left: &search.StringLiteral{Value: "req.body"},
|
||||
Right: &search.StringLiteral{Value: "bar"},
|
||||
},
|
||||
expectedSqlizer: sq.LtOrEq{"req.body": "bar"},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "req.body =~ bar",
|
||||
searchExpr: &search.InfixExpression{
|
||||
Operator: search.TokOpRe,
|
||||
Left: &search.StringLiteral{Value: "req.body"},
|
||||
Right: &search.StringLiteral{Value: "bar"},
|
||||
},
|
||||
expectedSqlizer: sq.Expr("regexp(?, req.body)", "bar"),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "req.body !~ bar",
|
||||
searchExpr: &search.InfixExpression{
|
||||
Operator: search.TokOpNotRe,
|
||||
Left: &search.StringLiteral{Value: "req.body"},
|
||||
Right: &search.StringLiteral{Value: "bar"},
|
||||
},
|
||||
expectedSqlizer: sq.Expr("NOT regexp(?, req.body)", "bar"),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "req.body = bar AND res.body = yolo",
|
||||
searchExpr: &search.InfixExpression{
|
||||
Operator: search.TokOpAnd,
|
||||
Left: &search.InfixExpression{
|
||||
Operator: search.TokOpEq,
|
||||
Left: &search.StringLiteral{Value: "req.body"},
|
||||
Right: &search.StringLiteral{Value: "bar"},
|
||||
},
|
||||
Right: &search.InfixExpression{
|
||||
Operator: search.TokOpEq,
|
||||
Left: &search.StringLiteral{Value: "res.body"},
|
||||
Right: &search.StringLiteral{Value: "yolo"},
|
||||
},
|
||||
},
|
||||
expectedSqlizer: sq.And{
|
||||
sq.Eq{"req.body": "bar"},
|
||||
sq.Eq{"res.body": "yolo"},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "req.body = bar AND res.body = yolo AND req.method = POST",
|
||||
searchExpr: &search.InfixExpression{
|
||||
Operator: search.TokOpAnd,
|
||||
Left: &search.InfixExpression{
|
||||
Operator: search.TokOpEq,
|
||||
Left: &search.StringLiteral{Value: "req.body"},
|
||||
Right: &search.StringLiteral{Value: "bar"},
|
||||
},
|
||||
Right: &search.InfixExpression{
|
||||
Operator: search.TokOpAnd,
|
||||
Left: &search.InfixExpression{
|
||||
Operator: search.TokOpEq,
|
||||
Left: &search.StringLiteral{Value: "res.body"},
|
||||
Right: &search.StringLiteral{Value: "yolo"},
|
||||
},
|
||||
Right: &search.InfixExpression{
|
||||
Operator: search.TokOpEq,
|
||||
Left: &search.StringLiteral{Value: "req.method"},
|
||||
Right: &search.StringLiteral{Value: "POST"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedSqlizer: sq.And{
|
||||
sq.Eq{"req.body": "bar"},
|
||||
sq.And{
|
||||
sq.Eq{"res.body": "yolo"},
|
||||
sq.Eq{"req.method": "POST"},
|
||||
},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "req.body = bar OR res.body = yolo",
|
||||
searchExpr: &search.InfixExpression{
|
||||
Operator: search.TokOpOr,
|
||||
Left: &search.InfixExpression{
|
||||
Operator: search.TokOpEq,
|
||||
Left: &search.StringLiteral{Value: "req.body"},
|
||||
Right: &search.StringLiteral{Value: "bar"},
|
||||
},
|
||||
Right: &search.InfixExpression{
|
||||
Operator: search.TokOpEq,
|
||||
Left: &search.StringLiteral{Value: "res.body"},
|
||||
Right: &search.StringLiteral{Value: "yolo"},
|
||||
},
|
||||
},
|
||||
expectedSqlizer: sq.Or{
|
||||
sq.Eq{"req.body": "bar"},
|
||||
sq.Eq{"res.body": "yolo"},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got, err := parseSearchExpr(tt.searchExpr)
|
||||
assertError(t, tt.expectedError, err)
|
||||
if !reflect.DeepEqual(tt.expectedSqlizer, got) {
|
||||
t.Errorf("expected: %#v, got: %#v", tt.expectedSqlizer, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertError(t *testing.T, exp, got error) {
|
||||
switch {
|
||||
case exp == nil && got != nil:
|
||||
t.Fatalf("expected: nil, got: %v", got)
|
||||
case exp != nil && got == nil:
|
||||
t.Fatalf("expected: %v, got: nil", exp.Error())
|
||||
case exp != nil && got != nil && exp.Error() != got.Error():
|
||||
t.Fatalf("expected: %v, got: %v", exp.Error(), got.Error())
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ var regexpFn = func(pattern string, value interface{}) (bool, error) {
|
|||
case string:
|
||||
return regexp.MatchString(pattern, v)
|
||||
case int64:
|
||||
return regexp.MatchString(pattern, string(v))
|
||||
return regexp.MatchString(pattern, fmt.Sprintf("%v", v))
|
||||
case []byte:
|
||||
return regexp.Match(pattern, v)
|
||||
default:
|
||||
|
@ -257,6 +257,14 @@ func (c *Client) FindRequestLogs(
|
|||
}
|
||||
}
|
||||
|
||||
if filter.SearchExpr != nil {
|
||||
sqlizer, err := parseSearchExpr(filter.SearchExpr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sqlite: could not parse search expression: %v", err)
|
||||
}
|
||||
reqQuery = reqQuery.Where(sqlizer)
|
||||
}
|
||||
|
||||
sql, args, err := reqQuery.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sqlite: could not parse query: %v", err)
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
@ -14,6 +15,7 @@ import (
|
|||
"github.com/dstotijn/hetty/pkg/proj"
|
||||
"github.com/dstotijn/hetty/pkg/proxy"
|
||||
"github.com/dstotijn/hetty/pkg/scope"
|
||||
"github.com/dstotijn/hetty/pkg/search"
|
||||
)
|
||||
|
||||
type contextKey int
|
||||
|
@ -51,7 +53,9 @@ type Service struct {
|
|||
}
|
||||
|
||||
type FindRequestsFilter struct {
|
||||
OnlyInScope bool
|
||||
OnlyInScope bool
|
||||
SearchExpr search.Expression `json:"-"`
|
||||
RawSearchExpr string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
|
@ -210,6 +214,34 @@ func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon
|
|||
}
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (f *FindRequestsFilter) UnmarshalJSON(b []byte) error {
|
||||
var dto struct {
|
||||
OnlyInScope bool
|
||||
RawSearchExpr string
|
||||
}
|
||||
if err := json.Unmarshal(b, &dto); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filter := FindRequestsFilter{
|
||||
OnlyInScope: dto.OnlyInScope,
|
||||
RawSearchExpr: dto.RawSearchExpr,
|
||||
}
|
||||
|
||||
if dto.RawSearchExpr != "" {
|
||||
expr, err := search.ParseQuery(dto.RawSearchExpr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filter.SearchExpr = expr
|
||||
}
|
||||
|
||||
*f = filter
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) loadSettings() error {
|
||||
return svc.repo.FindSettingsByModule(context.Background(), moduleName, svc)
|
||||
}
|
||||
|
|
|
@ -9,10 +9,10 @@ type precedence int
|
|||
const (
|
||||
_ precedence = iota
|
||||
precLowest
|
||||
precEq
|
||||
precAnd
|
||||
precOr
|
||||
precNot
|
||||
precEq
|
||||
precLessGreater
|
||||
precPrefix
|
||||
precGroup
|
||||
|
@ -86,7 +86,7 @@ func ParseQuery(input string) (expr Expression, err error) {
|
|||
p.nextToken()
|
||||
|
||||
if p.curTokenIs(TokEOF) {
|
||||
return nil, fmt.Errorf("unexpected EOF")
|
||||
return nil, fmt.Errorf("search: unexpected EOF")
|
||||
}
|
||||
|
||||
for !p.curTokenIs(TokEOF) {
|
||||
|
|
|
@ -17,7 +17,7 @@ func TestParseQuery(t *testing.T) {
|
|||
name: "empty query",
|
||||
input: "",
|
||||
expectedExpression: nil,
|
||||
expectedError: errors.New("unexpected EOF"),
|
||||
expectedError: errors.New("search: unexpected EOF"),
|
||||
},
|
||||
{
|
||||
name: "string literal expression",
|
||||
|
@ -199,6 +199,24 @@ func TestParseQuery(t *testing.T) {
|
|||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "eq operator takes precedence over boolean ops",
|
||||
input: "foo=bar OR baz=yolo",
|
||||
expectedExpression: &InfixExpression{
|
||||
Operator: TokOpOr,
|
||||
Left: &InfixExpression{
|
||||
Operator: TokOpEq,
|
||||
Left: &StringLiteral{Value: "foo"},
|
||||
Right: &StringLiteral{Value: "bar"},
|
||||
},
|
||||
Right: &InfixExpression{
|
||||
Operator: TokOpEq,
|
||||
Left: &StringLiteral{Value: "baz"},
|
||||
Right: &StringLiteral{Value: "yolo"},
|
||||
},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
Loading…
Reference in a new issue