Files
hetty/pkg/api/resolvers.go

394 lines
9.8 KiB
Go
Raw Normal View History

package api
//go:generate go run github.com/99designs/gqlgen
import (
"context"
2021-04-25 16:23:53 +02:00
"errors"
"fmt"
2020-10-29 20:54:17 +01:00
"regexp"
"strings"
2020-10-11 17:09:39 +02:00
"github.com/99designs/gqlgen/graphql"
2022-01-21 11:45:54 +01:00
"github.com/oklog/ulid"
2021-04-25 16:23:53 +02:00
"github.com/vektah/gqlparser/v2/gqlerror"
2020-10-11 17:09:39 +02:00
"github.com/dstotijn/hetty/pkg/proj"
2020-09-22 18:33:02 +02:00
"github.com/dstotijn/hetty/pkg/reqlog"
2020-10-29 20:54:17 +01:00
"github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/search"
)
type Resolver struct {
2022-01-01 16:11:49 +01:00
ProjectService proj.Service
2022-01-21 11:45:54 +01:00
RequestLogService *reqlog.Service
}
2021-04-25 16:23:53 +02:00
type (
queryResolver struct{ *Resolver }
mutationResolver struct{ *Resolver }
)
2020-10-11 17:09:39 +02:00
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
func (r *queryResolver) HTTPRequestLogs(ctx context.Context) ([]HTTPRequestLog, error) {
2020-10-29 20:54:17 +01:00
reqs, err := r.RequestLogService.FindRequests(ctx)
2021-04-25 16:23:53 +02:00
if errors.Is(err, proj.ErrNoProject) {
return nil, noActiveProjectErr(ctx)
2021-04-25 16:23:53 +02:00
} else if err != nil {
return nil, fmt.Errorf("could not query repository for requests: %w", err)
2020-10-11 17:09:39 +02:00
}
2021-04-25 16:23:53 +02:00
logs := make([]HTTPRequestLog, len(reqs))
for i, req := range reqs {
req, err := parseRequestLog(req)
if err != nil {
return nil, err
}
2021-04-25 16:23:53 +02:00
logs[i] = req
}
return logs, nil
}
2022-01-21 11:45:54 +01:00
func (r *queryResolver) HTTPRequestLog(ctx context.Context, id ULID) (*HTTPRequestLog, error) {
log, err := r.RequestLogService.FindRequestLogByID(ctx, ulid.ULID(id))
2021-04-25 16:23:53 +02:00
if errors.Is(err, reqlog.ErrRequestNotFound) {
return nil, nil
2021-04-25 16:23:53 +02:00
} else if err != nil {
return nil, fmt.Errorf("could not get request by ID: %w", err)
}
2021-04-25 16:23:53 +02:00
req, err := parseRequestLog(log)
if err != nil {
return nil, err
}
return &req, nil
}
2022-01-21 11:45:54 +01:00
func parseRequestLog(reqLog reqlog.RequestLog) (HTTPRequestLog, error) {
method := HTTPMethod(reqLog.Method)
if method != "" && !method.IsValid() {
return HTTPRequestLog{}, fmt.Errorf("request has invalid method: %v", method)
}
log := HTTPRequestLog{
2022-01-21 11:45:54 +01:00
ID: ULID(reqLog.ID),
Proto: reqLog.Proto,
Method: method,
2022-01-21 11:45:54 +01:00
Timestamp: ulid.Time(reqLog.ID.Time()),
}
2022-01-21 11:45:54 +01:00
if reqLog.URL != nil {
log.URL = reqLog.URL.String()
}
2022-01-21 11:45:54 +01:00
if len(reqLog.Body) > 0 {
bodyStr := string(reqLog.Body)
log.Body = &bodyStr
}
2022-01-21 11:45:54 +01:00
if reqLog.Header != nil {
log.Headers = make([]HTTPHeader, 0)
2021-04-25 16:23:53 +02:00
2022-01-21 11:45:54 +01:00
for key, values := range reqLog.Header {
for _, value := range values {
log.Headers = append(log.Headers, HTTPHeader{
Key: key,
Value: value,
})
}
}
}
2022-01-21 11:45:54 +01:00
if reqLog.Response != nil {
log.Response = &HTTPResponseLog{
2022-01-21 11:45:54 +01:00
Proto: reqLog.Response.Proto,
StatusCode: reqLog.Response.StatusCode,
}
2022-01-21 11:45:54 +01:00
statusReasonSubs := strings.SplitN(reqLog.Response.Status, " ", 2)
2021-04-25 16:23:53 +02:00
if len(statusReasonSubs) == 2 {
log.Response.StatusReason = statusReasonSubs[1]
}
2021-04-25 16:23:53 +02:00
2022-01-21 11:45:54 +01:00
if len(reqLog.Response.Body) > 0 {
bodyStr := string(reqLog.Response.Body)
log.Response.Body = &bodyStr
}
2021-04-25 16:23:53 +02:00
2022-01-21 11:45:54 +01:00
if reqLog.Response.Header != nil {
log.Response.Headers = make([]HTTPHeader, 0)
2021-04-25 16:23:53 +02:00
2022-01-21 11:45:54 +01:00
for key, values := range reqLog.Response.Header {
for _, value := range values {
log.Response.Headers = append(log.Response.Headers, HTTPHeader{
Key: key,
Value: value,
})
}
}
}
}
return log, nil
}
2020-10-11 17:09:39 +02:00
2022-01-21 11:45:54 +01:00
func (r *mutationResolver) CreateProject(ctx context.Context, name string) (*Project, error) {
p, err := r.ProjectService.CreateProject(ctx, name)
2021-04-25 16:23:53 +02:00
if errors.Is(err, proj.ErrInvalidName) {
2020-10-11 17:09:39 +02:00
return nil, gqlerror.Errorf("Project name must only contain alphanumeric or space chars.")
2021-04-25 16:23:53 +02:00
} else if err != nil {
return nil, fmt.Errorf("could not open project: %w", err)
2020-10-11 17:09:39 +02:00
}
2021-04-25 16:23:53 +02:00
2020-10-11 17:09:39 +02:00
return &Project{
2022-01-21 11:45:54 +01:00
ID: ULID(p.ID),
2020-10-11 17:09:39 +02:00
Name: p.Name,
2022-01-21 11:45:54 +01:00
IsActive: r.ProjectService.IsProjectActive(p.ID),
}, nil
}
func (r *mutationResolver) OpenProject(ctx context.Context, id ULID) (*Project, error) {
p, err := r.ProjectService.OpenProject(ctx, ulid.ULID(id))
if errors.Is(err, proj.ErrInvalidName) {
return nil, gqlerror.Errorf("Project name must only contain alphanumeric or space chars.")
} else if err != nil {
return nil, fmt.Errorf("could not open project: %w", err)
}
return &Project{
ID: ULID(p.ID),
Name: p.Name,
IsActive: r.ProjectService.IsProjectActive(p.ID),
2020-10-11 17:09:39 +02:00
}, nil
}
func (r *queryResolver) ActiveProject(ctx context.Context) (*Project, error) {
2022-01-21 11:45:54 +01:00
p, err := r.ProjectService.ActiveProject(ctx)
2021-04-25 16:23:53 +02:00
if errors.Is(err, proj.ErrNoProject) {
2020-10-11 17:09:39 +02:00
return nil, nil
2021-04-25 16:23:53 +02:00
} else if err != nil {
return nil, fmt.Errorf("could not open project: %w", err)
2020-10-11 17:09:39 +02:00
}
return &Project{
2022-01-21 11:45:54 +01:00
ID: ULID(p.ID),
2020-10-11 17:09:39 +02:00
Name: p.Name,
2022-01-21 11:45:54 +01:00
IsActive: r.ProjectService.IsProjectActive(p.ID),
2020-10-11 17:09:39 +02:00
}, nil
}
func (r *queryResolver) Projects(ctx context.Context) ([]Project, error) {
2022-01-21 11:45:54 +01:00
p, err := r.ProjectService.Projects(ctx)
2020-10-11 17:09:39 +02:00
if err != nil {
2021-04-25 16:23:53 +02:00
return nil, fmt.Errorf("could not get projects: %w", err)
2020-10-11 17:09:39 +02:00
}
projects := make([]Project, len(p))
for i, proj := range p {
projects[i] = Project{
2022-01-21 11:45:54 +01:00
ID: ULID(proj.ID),
2020-10-11 17:09:39 +02:00
Name: proj.Name,
2022-01-21 11:45:54 +01:00
IsActive: r.ProjectService.IsProjectActive(proj.ID),
2020-10-11 17:09:39 +02:00
}
}
return projects, nil
}
2020-10-29 20:54:17 +01:00
func (r *queryResolver) Scope(ctx context.Context) ([]ScopeRule, error) {
2022-01-21 11:45:54 +01:00
rules := r.ProjectService.Scope().Rules()
2020-10-29 20:54:17 +01:00
return scopeToScopeRules(rules), nil
}
func regexpToStringPtr(r *regexp.Regexp) *string {
if r == nil {
return nil
}
2021-04-25 16:23:53 +02:00
2020-10-29 20:54:17 +01:00
s := r.String()
2021-04-25 16:23:53 +02:00
2020-10-29 20:54:17 +01:00
return &s
}
2020-10-11 17:09:39 +02:00
func (r *mutationResolver) CloseProject(ctx context.Context) (*CloseProjectResult, error) {
2022-01-21 11:45:54 +01:00
if err := r.ProjectService.CloseProject(); err != nil {
2021-04-25 16:23:53 +02:00
return nil, fmt.Errorf("could not close project: %w", err)
2020-10-11 17:09:39 +02:00
}
2021-04-25 16:23:53 +02:00
2020-10-11 17:09:39 +02:00
return &CloseProjectResult{true}, nil
}
2022-01-21 11:45:54 +01:00
func (r *mutationResolver) DeleteProject(ctx context.Context, id ULID) (*DeleteProjectResult, error) {
if err := r.ProjectService.DeleteProject(ctx, ulid.ULID(id)); err != nil {
2021-04-25 16:23:53 +02:00
return nil, fmt.Errorf("could not delete project: %w", err)
2020-10-11 17:09:39 +02:00
}
2021-04-25 16:23:53 +02:00
2020-10-11 17:09:39 +02:00
return &DeleteProjectResult{
Success: true,
}, nil
}
2020-10-29 20:54:17 +01:00
func (r *mutationResolver) ClearHTTPRequestLog(ctx context.Context) (*ClearHTTPRequestLogResult, error) {
2022-01-21 11:45:54 +01:00
project, err := r.ProjectService.ActiveProject(ctx)
if errors.Is(err, proj.ErrNoProject) {
return nil, noActiveProjectErr(ctx)
} else if err != nil {
return nil, fmt.Errorf("could not get active project: %w", err)
}
if err := r.RequestLogService.ClearRequests(ctx, project.ID); err != nil {
2021-04-25 16:23:53 +02:00
return nil, fmt.Errorf("could not clear request log: %w", err)
}
2021-04-25 16:23:53 +02:00
return &ClearHTTPRequestLogResult{true}, nil
}
2020-10-29 20:54:17 +01:00
func (r *mutationResolver) SetScope(ctx context.Context, input []ScopeRuleInput) ([]ScopeRule, error) {
rules := make([]scope.Rule, len(input))
2021-04-25 16:23:53 +02:00
2020-10-29 20:54:17 +01:00
for i, rule := range input {
u, err := stringPtrToRegexp(rule.URL)
if err != nil {
2021-04-25 16:23:53 +02:00
return nil, fmt.Errorf("invalid URL in scope rule: %w", err)
2020-10-29 20:54:17 +01:00
}
2021-04-25 16:23:53 +02:00
2020-10-29 20:54:17 +01:00
var headerKey, headerValue *regexp.Regexp
2021-04-25 16:23:53 +02:00
2020-10-29 20:54:17 +01:00
if rule.Header != nil {
headerKey, err = stringPtrToRegexp(rule.Header.Key)
if err != nil {
2021-04-25 16:23:53 +02:00
return nil, fmt.Errorf("invalid header key in scope rule: %w", err)
2020-10-29 20:54:17 +01:00
}
2021-04-25 16:23:53 +02:00
2020-10-29 20:54:17 +01:00
headerValue, err = stringPtrToRegexp(rule.Header.Key)
if err != nil {
2021-04-25 16:23:53 +02:00
return nil, fmt.Errorf("invalid header value in scope rule: %w", err)
2020-10-29 20:54:17 +01:00
}
}
2021-04-25 16:23:53 +02:00
2020-10-29 20:54:17 +01:00
body, err := stringPtrToRegexp(rule.Body)
if err != nil {
2021-04-25 16:23:53 +02:00
return nil, fmt.Errorf("invalid body in scope rule: %w", err)
2020-10-29 20:54:17 +01:00
}
2021-04-25 16:23:53 +02:00
2020-10-29 20:54:17 +01:00
rules[i] = scope.Rule{
URL: u,
Header: scope.Header{
Key: headerKey,
Value: headerValue,
},
Body: body,
}
}
2022-01-21 11:45:54 +01:00
err := r.ProjectService.SetScopeRules(ctx, rules)
if err != nil {
return nil, fmt.Errorf("could not set scope rules: %w", err)
2020-10-29 20:54:17 +01:00
}
return scopeToScopeRules(rules), nil
}
func (r *queryResolver) HTTPRequestLogFilter(ctx context.Context) (*HTTPRequestLogFilter, error) {
return findReqFilterToHTTPReqLogFilter(r.RequestLogService.FindReqsFilter), nil
}
func (r *mutationResolver) SetHTTPRequestLogFilter(
ctx context.Context,
input *HTTPRequestLogFilterInput,
) (*HTTPRequestLogFilter, error) {
filter, err := findRequestsFilterFromInput(input)
if err != nil {
2021-04-25 16:23:53 +02:00
return nil, fmt.Errorf("could not parse request log filter: %w", err)
}
2021-04-25 16:23:53 +02:00
2022-01-21 11:45:54 +01:00
err = r.ProjectService.SetRequestLogFindFilter(ctx, filter)
2021-04-25 16:23:53 +02:00
if errors.Is(err, proj.ErrNoProject) {
return nil, noActiveProjectErr(ctx)
2021-04-25 16:23:53 +02:00
} else if err != nil {
return nil, fmt.Errorf("could not set request log filter: %w", err)
2020-10-29 20:54:17 +01:00
}
return findReqFilterToHTTPReqLogFilter(filter), nil
}
func stringPtrToRegexp(s *string) (*regexp.Regexp, error) {
if s == nil {
return nil, nil
}
2021-04-25 16:23:53 +02:00
2020-10-29 20:54:17 +01:00
return regexp.Compile(*s)
}
func scopeToScopeRules(rules []scope.Rule) []ScopeRule {
scopeRules := make([]ScopeRule, len(rules))
for i, rule := range rules {
scopeRules[i].URL = regexpToStringPtr(rule.URL)
if rule.Header.Key != nil || rule.Header.Value != nil {
scopeRules[i].Header = &ScopeHeader{
Key: regexpToStringPtr(rule.Header.Key),
Value: regexpToStringPtr(rule.Header.Value),
}
}
2021-04-25 16:23:53 +02:00
2020-10-29 20:54:17 +01:00
scopeRules[i].Body = regexpToStringPtr(rule.Body)
}
2021-04-25 16:23:53 +02:00
2020-10-29 20:54:17 +01:00
return scopeRules
}
func findRequestsFilterFromInput(input *HTTPRequestLogFilterInput) (filter reqlog.FindRequestsFilter, err error) {
2020-10-29 20:54:17 +01:00
if input == nil {
return
}
2021-04-25 16:23:53 +02:00
2020-10-29 20:54:17 +01:00
if input.OnlyInScope != nil {
filter.OnlyInScope = *input.OnlyInScope
}
2021-04-25 16:23:53 +02:00
if input.SearchExpression != nil && *input.SearchExpression != "" {
expr, err := search.ParseQuery(*input.SearchExpression)
if err != nil {
2021-04-25 16:23:53 +02:00
return reqlog.FindRequestsFilter{}, fmt.Errorf("could not parse search query: %w", err)
}
2021-04-25 16:23:53 +02:00
filter.SearchExpr = expr
}
2020-10-29 20:54:17 +01:00
return
}
func findReqFilterToHTTPReqLogFilter(findReqFilter reqlog.FindRequestsFilter) *HTTPRequestLogFilter {
empty := reqlog.FindRequestsFilter{}
if findReqFilter == empty {
return nil
}
2021-04-25 16:23:53 +02:00
2020-10-29 20:54:17 +01:00
httpReqLogFilter := &HTTPRequestLogFilter{
OnlyInScope: findReqFilter.OnlyInScope,
}
2022-01-21 11:45:54 +01:00
if findReqFilter.SearchExpr != nil {
searchExpr := findReqFilter.SearchExpr.String()
httpReqLogFilter.SearchExpression = &searchExpr
}
2020-10-29 20:54:17 +01:00
return httpReqLogFilter
}
func noActiveProjectErr(ctx context.Context) error {
return &gqlerror.Error{
Path: graphql.GetPath(ctx),
Message: "No active project.",
Extensions: map[string]interface{}{
"code": "no_active_project",
},
}
}