Skip to content

Commit

Permalink
#4222: cleaned up URL paths for CRUD on filter configs
Browse files Browse the repository at this point in the history
  • Loading branch information
sreuland committed Feb 24, 2022
1 parent 3e345ba commit b2deb81
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 37 deletions.
149 changes: 120 additions & 29 deletions services/horizon/internal/actions/filter_rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package actions

import (
"context"
"database/sql"
"encoding/json"
"fmt"
"net/http"

horizonContext "github.com/stellar/go/services/horizon/internal/context"
Expand All @@ -12,16 +14,20 @@ import (

// standard resource interface for a filter config
type filterResource struct {
Rules map[string]interface{} `json:"rules"`
Enabled bool `json:"enabled"`
Name string `json:"name"`
LastModified int64 `json:"last_modified,omitempty"`
Rules map[string]interface{} `json:"rules"`
Enabled bool `json:"enabled"`
Name string `json:"name,omitempty"`
LastModified int64 `json:"last_modified,omitempty"`
}

type FilterQuery struct {
type QueryPathParams struct {
NAME string `schema:"name" valid:"optional"`
}

type UpdatePathParams struct {
NAME string `schema:"name" valid:"required"`
}

type FilterRuleHandler struct{}

func (handler FilterRuleHandler) Get(w http.ResponseWriter, r *http.Request) {
Expand All @@ -31,17 +37,17 @@ func (handler FilterRuleHandler) Get(w http.ResponseWriter, r *http.Request) {
return
}

qp := FilterQuery{}
err = getParams(&qp, r)
pp := QueryPathParams{}
err = getParams(&pp, r)
if err != nil {
problem.Render(r.Context(), w, err)
return
}

var responsePayload interface{}

if (qp.NAME != "") {
responsePayload, err = handler.findOne(qp.NAME, historyQ, r.Context())
if pp.NAME != "" {
responsePayload, err = handler.findOne(pp.NAME, historyQ, r.Context())
} else {
responsePayload, err = handler.findAll(historyQ, r.Context())
}
Expand All @@ -56,27 +62,117 @@ func (handler FilterRuleHandler) Get(w http.ResponseWriter, r *http.Request) {
}
}

func (handler FilterRuleHandler) Set(w http.ResponseWriter, r *http.Request) {
func (handler FilterRuleHandler) Create(w http.ResponseWriter, r *http.Request) {
historyQ, err := horizonContext.HistoryQFromRequest(r)
if err != nil {
problem.Render(r.Context(), w, err)
return
}
var filterRequest filterResource
dec := json.NewDecoder(r.Body)
if err = dec.Decode(&filterRequest); err != nil {

filterRequest, err := handler.requestedFilter(r)
if err != nil {
problem.Render(r.Context(), w, err)
}

existing, err := handler.findOne(filterRequest.Name, historyQ, r.Context())
if err != sql.ErrNoRows {
if existing != nil {
err := problem.BadRequest
err.Extras = map[string]interface{}{
"filter already exists": filterRequest.Name,
}
}
problem.Render(r.Context(), w, err)
return
}

if err = handler.upsert(filterRequest, historyQ, r.Context()); err != nil {
problem.Render(r.Context(), w, err)
}
w.WriteHeader(201)
}

func (handler FilterRuleHandler) Update(w http.ResponseWriter, r *http.Request) {
historyQ, err := horizonContext.HistoryQFromRequest(r)
if err != nil {
problem.Render(r.Context(), w, err)
return
}

pp := &UpdatePathParams{}
err = getParams(pp, r)
if err != nil {
problem.Render(r.Context(), w, err)
return
}

filterRequest, err := handler.requestedFilter(r)
if err != nil {
problem.Render(r.Context(), w, err)
return
}

if pp.NAME != filterRequest.Name {
p := problem.BadRequest
p.Extras = map[string]interface{}{
"invalid json for filter config": err.Error(),
"reason": fmt.Sprintf("url path %v, does not match body value %v", pp.NAME, filterRequest.Name),
}
problem.Render(r.Context(), w, p)
return
}

if _, err = handler.findOne(filterRequest.Name, historyQ, r.Context()); err != nil {
// not found or other error
problem.Render(r.Context(), w, err)
}

if err = handler.upsert(filterRequest, historyQ, r.Context()); err != nil {
problem.Render(r.Context(), w, err)
}
}

func (handler FilterRuleHandler) Delete(w http.ResponseWriter, r *http.Request) {
historyQ, err := horizonContext.HistoryQFromRequest(r)
if err != nil {
problem.Render(r.Context(), w, err)
return
}

pp := &UpdatePathParams{}
err = getParams(pp, r)
if err != nil {
problem.Render(r.Context(), w, err)
return
}

if _, err = handler.findOne(pp.NAME, historyQ, r.Context()); err != nil {
// not found or other error
problem.Render(r.Context(), w, err)
}

if err = historyQ.DeleteFilterByName(r.Context(), pp.NAME); err != nil {
problem.Render(r.Context(), w, err)
}
w.WriteHeader(204)
}

func (handler FilterRuleHandler) requestedFilter(r *http.Request) (*filterResource, error) {
filterRequest := &filterResource{}
dec := json.NewDecoder(r.Body)
if err := dec.Decode(filterRequest); err != nil {
p := problem.BadRequest
p.Extras = map[string]interface{}{
"reason": fmt.Sprintf("invalid json for filter config %v", err.Error()),
}
return nil, p
}
return filterRequest, nil
}

func (handler FilterRuleHandler) upsert(filterRequest *filterResource, historyQ *history.Q, ctx context.Context) error {
//TODO, consider type specific schema validation of the json in filterRequest.Rules based on filterRequest.Name
// if name='asset', verify against an Asset Config Struct
// if name='account', verify against an Account Config Struct

filterConfig := history.FilterConfig{}
filterConfig.Enabled = filterRequest.Enabled
filterConfig.Name = filterRequest.Name
Expand All @@ -85,21 +181,16 @@ func (handler FilterRuleHandler) Set(w http.ResponseWriter, r *http.Request) {
if err != nil {
p := problem.ServerError
p.Extras = map[string]interface{}{
"reason": "unable to serialize asset filter rules resource from json",
"reason": fmt.Sprintf("unable to serialize filter rules resource from json %v", err.Error()),
}
problem.Render(r.Context(), w, err)
return
return p
}
filterConfig.Rules = string(filterRules)

if err = historyQ.SetFilterConfig(r.Context(), filterConfig); err != nil {
problem.Render(r.Context(), w, err)
return
}
return historyQ.UpsertFilterConfig(ctx, filterConfig)
}

func (handler FilterRuleHandler) findOne(name string, historyQ *history.Q, ctx context.Context) (*filterResource, error) {
filter, err := historyQ.GetFilterByName(ctx,name)
filter, err := historyQ.GetFilterByName(ctx, name)
if err != nil {
return nil, err
}
Expand All @@ -110,7 +201,7 @@ func (handler FilterRuleHandler) findOne(name string, historyQ *history.Q, ctx c
return handler.resource(filter, rules), nil
}

func (handler FilterRuleHandler) findAll(historyQ *history.Q, ctx context.Context) ([]*filterResource, error){
func (handler FilterRuleHandler) findAll(historyQ *history.Q, ctx context.Context) ([]*filterResource, error) {
configs, err := historyQ.GetAllFilters(ctx)
if err != nil {
return nil, err
Expand Down Expand Up @@ -138,11 +229,11 @@ func (handler FilterRuleHandler) rules(input string) (map[string]interface{}, er
return rules, nil
}

func (handler FilterRuleHandler) resource(config history.FilterConfig, rules map[string]interface{}) *filterResource{
func (handler FilterRuleHandler) resource(config history.FilterConfig, rules map[string]interface{}) *filterResource {
return &filterResource{
Rules: rules,
Enabled: config.Enabled,
Name: config.Name,
Rules: rules,
Enabled: config.Enabled,
Name: config.Name,
LastModified: config.LastModified,
}
}
42 changes: 40 additions & 2 deletions services/horizon/internal/db2/history/filter_rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package history

import (
"context"
"fmt"

sq "github.com/Masterminds/squirrel"
"github.com/stellar/go/support/errors"
"github.com/stellar/go/support/render/problem"
)

const (
Expand All @@ -17,6 +19,10 @@ const (
FilterAccountFilterName = "account"
)

var (
supportedNames = []string{FilterAssetFilterName, FilterAccountFilterName}
)

type FilterConfig struct {
Enabled bool `db:"enabled"`
Rules string `db:"rules"`
Expand All @@ -27,7 +33,8 @@ type FilterConfig struct {
type QFilter interface {
GetAllFilters(ctx context.Context) ([]FilterConfig, error)
GetFilterByName(ctx context.Context, name string) (FilterConfig, error)
SetFilterConfig(ctx context.Context, config FilterConfig) error
UpsertFilterConfig(ctx context.Context, config FilterConfig) error
DeleteFilterByName(ctx context.Context, name string) error
}

func (q *Q) GetAllFilters(ctx context.Context) ([]FilterConfig, error) {
Expand All @@ -46,14 +53,36 @@ func (q *Q) GetFilterByName(ctx context.Context, name string) (FilterConfig, err
return filterConfig, err
}

func (q *Q) SetFilterConfig(ctx context.Context, config FilterConfig) error {
func (q *Q) DeleteFilterByName(ctx context.Context, name string) error {
sql := sq.Delete(filterRulesTableName).Where(sq.Eq{filterRulesTypeColumnName: name})
rowCnt, err := q.checkForError(sql, ctx)

if err != nil {
return err
}

if rowCnt < 1 {
return errors.Errorf("deletion of filter rule did not result any rows affected")
}
return nil
}

func (q *Q) UpsertFilterConfig(ctx context.Context, config FilterConfig) error {
updateCols := map[string]interface{}{
filterRulesLastModifiedColumnName: sq.Expr("extract(epoch from now() at time zone 'utc')"),
filterRulesEnabledColumnName: config.Enabled,
filterRulesColumnName: config.Rules,
filterRulesTypeColumnName: config.Name,
}

if !q.supportedFilterNames(config.Name) {
p := problem.ServerError
p.Extras = map[string]interface{}{
"reason": fmt.Sprintf("invalid filter name, %v, no implementation for this exists", config.Name),
}
return p
}

sqlUpdate := sq.Update(filterRulesTableName).SetMap(updateCols).Where(
sq.Eq{filterRulesTypeColumnName: config.Name})

Expand Down Expand Up @@ -82,3 +111,12 @@ func (q *Q) checkForError(builder sq.Sqlizer, ctx context.Context) (int64, error
}
return result.RowsAffected()
}

func (q *Q) supportedFilterNames(name string) bool {
for _, supportedName := range supportedNames {
if name == supportedName {
return true
}
}
return false
}
7 changes: 6 additions & 1 deletion services/horizon/internal/db2/history/mock_q_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ func (m *MockQFilter) GetFilterByName(ctx context.Context, name string) (FilterC
return a.Get(0).(FilterConfig), a.Error(1)
}

func (m *MockQFilter) SetFilterConfig(ctx context.Context, config FilterConfig) error {
func (m *MockQFilter) UpsertFilterConfig(ctx context.Context, config FilterConfig) error {
a := m.Called(ctx, config)
return a.Error(0)
}

func (m *MockQFilter) DeleteFilterByName(ctx context.Context, name string) error {
a := m.Called(ctx, name)
return a.Error(0)
}
14 changes: 9 additions & 5 deletions services/horizon/internal/httpx/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,13 +343,17 @@ func (r *Router) addRoutes(config *RouterConfig, rateLimiter *throttled.HTTPRate
r.Internal.Get("/debug/pprof/profile", pprof.Profile)
if config.EnableIngestionFiltering {
r.Internal.Route("/ingestion/filters", func(r chi.Router) {
// 3 paths
// GetAll: GET /ingestion/filters/ = response of list of {Name, Rules, Enabled, LastModified}
// GetOne: GET /ingestion/filters/{name} = response of {Name, Rules, Enabled, LastModified} with Name='name' or NOT Found err
// Upsert One: POST /ingestion/filters, http body = {Name, Rules, Enabled}, response of {Name, Rules, Enabled, LastModified}
// 5 paths
// POST /intestion/filters
// GET /ingestion/filters/{name}
// GET /ingestion/filters
// PUT /ingestion/filters/{name}
// DELETE /ingestion/filters/{name}
handler := actions.FilterRuleHandler{}
r.With(historyMiddleware).Post("/", handler.Set)
r.With(historyMiddleware).Post("/", handler.Create)
r.With(historyMiddleware).Put("/", handler.Update)
r.With(historyMiddleware).Get("/", handler.Get)
r.With(historyMiddleware).Delete("/", handler.Delete)
})
}
}

0 comments on commit b2deb81

Please sign in to comment.