From e822ad7c064137879e09fdd0a6bdc5a97f922a76 Mon Sep 17 00:00:00 2001 From: Peter Grlica Date: Fri, 28 Jan 2022 12:36:56 +0100 Subject: [PATCH] Fixed read request body, added read request body wf function, added more tests --- .../automation/apigw_body_handler.gen.go | 109 ++++ automation/automation/apigw_body_handler.go | 41 ++ automation/automation/apigw_body_handler.yaml | 17 + automation/automation/expr_types.gen.go | 578 ------------------ automation/automation/expr_types.go | 88 +-- automation/automation/expr_types.yaml | 32 +- automation/automation/jsenv_handler.go | 2 - automation/automation/jsenv_handler_test.go | 201 ++++++ automation/service/service.go | 3 +- pkg/apigw/filter/postfilter.go | 3 +- pkg/apigw/filter/postfilter_test.go | 11 +- pkg/apigw/filter/processer.go | 77 +-- pkg/apigw/filter/processer_test.go | 123 +++- pkg/apigw/registry/registry.go | 6 +- pkg/apigw/route.go | 8 +- pkg/expr/expr_types.gen.go | 248 ++++++++ pkg/expr/expr_types.go | 31 + pkg/expr/expr_types.yaml | 14 + pkg/expr/vars.go | 34 +- pkg/http/client.go | 2 - pkg/http/request.go | 91 +++ pkg/http/request_test.go | 40 ++ store/rdbms/reports.gen.go | 1 - tests/apigw/main_test.go | 2 - 24 files changed, 969 insertions(+), 793 deletions(-) create mode 100644 automation/automation/apigw_body_handler.gen.go create mode 100644 automation/automation/apigw_body_handler.go create mode 100644 automation/automation/apigw_body_handler.yaml create mode 100644 automation/automation/jsenv_handler_test.go create mode 100644 pkg/http/request.go create mode 100644 pkg/http/request_test.go diff --git a/automation/automation/apigw_body_handler.gen.go b/automation/automation/apigw_body_handler.gen.go new file mode 100644 index 000000000..1989c0c60 --- /dev/null +++ b/automation/automation/apigw_body_handler.gen.go @@ -0,0 +1,109 @@ +package automation + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// automation/automation/apigw_body_handler.yaml + +import ( + "context" + atypes "github.com/cortezaproject/corteza-server/automation/types" + "github.com/cortezaproject/corteza-server/pkg/expr" + "github.com/cortezaproject/corteza-server/pkg/http" + "github.com/cortezaproject/corteza-server/pkg/wfexec" +) + +var _ wfexec.ExecResponse + +type ( + apigwBodyHandlerRegistry interface { + AddFunctions(ff ...*atypes.Function) + Type(ref string) expr.Type + } +) + +func (h apigwBodyHandler) register() { + h.reg.AddFunctions( + h.Read(), + ) +} + +type ( + apigwBodyReadArgs struct { + hasRequest bool + Request *http.Request + } + + apigwBodyReadResults struct { + Body string + } +) + +// Read function Read request body from integration gateway +// +// expects implementation of read function: +// func (h apigwBodyHandler) read(ctx context.Context, args *apigwBodyReadArgs) (results *apigwBodyReadResults, err error) { +// return +// } +func (h apigwBodyHandler) Read() *atypes.Function { + return &atypes.Function{ + Ref: "apigwBodyRead", + Kind: "function", + Labels: map[string]string(nil), + Meta: &atypes.FunctionMeta{ + Short: "Read request body from integration gateway", + }, + + Parameters: []*atypes.Param{ + { + Name: "request", + Types: []string{"HttpRequest"}, Required: true, + }, + }, + + Results: []*atypes.Param{ + + { + Name: "body", + Types: []string{"String"}, + }, + }, + + Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) { + var ( + args = &apigwBodyReadArgs{ + hasRequest: in.Has("request"), + } + ) + + if err = in.Decode(args); err != nil { + return + } + + var results *apigwBodyReadResults + if results, err = h.read(ctx, args); err != nil { + return + } + + out = &expr.Vars{} + + { + // converting results.Body (string) to String + var ( + tval expr.TypedValue + ) + + if tval, err = h.reg.Type("String").Cast(results.Body); err != nil { + return + } else if err = expr.Assign(out, "body", tval); err != nil { + return + } + } + + return + }, + } +} diff --git a/automation/automation/apigw_body_handler.go b/automation/automation/apigw_body_handler.go new file mode 100644 index 000000000..c06f10b43 --- /dev/null +++ b/automation/automation/apigw_body_handler.go @@ -0,0 +1,41 @@ +package automation + +import ( + "context" + "fmt" + "io" +) + +type ( + apigwBodyHandler struct { + reg apigwBodyHandlerRegistry + } +) + +func ApigwBodyHandler(reg queueHandlerRegistry) *apigwBodyHandler { + h := &apigwBodyHandler{ + reg: reg, + } + + h.register() + return h +} + +func (h apigwBodyHandler) read(ctx context.Context, args *apigwBodyReadArgs) (res *apigwBodyReadResults, err error) { + res = &apigwBodyReadResults{} + + if !args.hasRequest { + err = fmt.Errorf("could not read body, contents missing") + return + } + + bb, err := io.ReadAll(args.Request.Body) + + if err != nil { + return + } + + res.Body = string(bb) + + return +} diff --git a/automation/automation/apigw_body_handler.yaml b/automation/automation/apigw_body_handler.yaml new file mode 100644 index 000000000..72d61ad7a --- /dev/null +++ b/automation/automation/apigw_body_handler.yaml @@ -0,0 +1,17 @@ +name: apigwBody + +imports: + - github.com/cortezaproject/corteza-server/pkg/http + +functions: + read: + meta: + short: Read request body from integration gateway + params: + request: + required: true + types: + - { wf: HttpRequest, go: '*http.Request' } + results: + body: + wf: String diff --git a/automation/automation/expr_types.gen.go b/automation/automation/expr_types.gen.go index dea5f3e04..b96cb0580 100644 --- a/automation/automation/expr_types.gen.go +++ b/automation/automation/expr_types.gen.go @@ -11,7 +11,6 @@ package automation import ( "context" "fmt" - "github.com/cortezaproject/corteza-server/automation/types" . "github.com/cortezaproject/corteza-server/pkg/expr" "sync" ) @@ -67,580 +66,3 @@ func (t *EmailMessage) Assign(val interface{}) error { return nil } } - -// HttpRequest is an expression type, wrapper for *types.HttpRequest type -type HttpRequest struct { - value *types.HttpRequest - mux sync.RWMutex -} - -// NewHttpRequest creates new instance of HttpRequest expression type -func NewHttpRequest(val interface{}) (*HttpRequest, error) { - if c, err := CastToHttpRequest(val); err != nil { - return nil, fmt.Errorf("unable to create HttpRequest: %w", err) - } else { - return &HttpRequest{value: c}, nil - } -} - -// Get return underlying value on HttpRequest -func (t *HttpRequest) Get() interface{} { - t.mux.RLock() - defer t.mux.RUnlock() - return t.value -} - -// GetValue returns underlying value on HttpRequest -func (t *HttpRequest) GetValue() *types.HttpRequest { - t.mux.RLock() - defer t.mux.RUnlock() - return t.value -} - -// Type return type name -func (HttpRequest) Type() string { return "HttpRequest" } - -// Cast converts value to *types.HttpRequest -func (HttpRequest) Cast(val interface{}) (TypedValue, error) { - return NewHttpRequest(val) -} - -// Assign new value to HttpRequest -// -// value is first passed through CastToHttpRequest -func (t *HttpRequest) Assign(val interface{}) error { - if c, err := CastToHttpRequest(val); err != nil { - return err - } else { - t.value = c - return nil - } -} - -func (t *HttpRequest) AssignFieldValue(key string, val TypedValue) error { - t.mux.Lock() - defer t.mux.Unlock() - return assignToHttpRequest(t.value, key, val) -} - -// SelectGVal implements gval.Selector requirements -// -// It allows gval lib to access HttpRequest's underlying value (*types.HttpRequest) -// and it's fields -// -func (t *HttpRequest) SelectGVal(ctx context.Context, k string) (interface{}, error) { - t.mux.RLock() - defer t.mux.RUnlock() - return httpRequestGValSelector(t.value, k) -} - -// Select is field accessor for *types.HttpRequest -// -// Similar to SelectGVal but returns typed values -func (t *HttpRequest) Select(k string) (TypedValue, error) { - t.mux.RLock() - defer t.mux.RUnlock() - return httpRequestTypedValueSelector(t.value, k) -} - -func (t *HttpRequest) Has(k string) bool { - t.mux.RLock() - defer t.mux.RUnlock() - switch k { - case "Method": - return true - case "URL": - return true - case "Header": - return true - case "Body": - return true - case "Form": - return true - case "PostForm": - return true - } - return false -} - -// httpRequestGValSelector is field accessor for *types.HttpRequest -func httpRequestGValSelector(res *types.HttpRequest, k string) (interface{}, error) { - if res == nil { - return nil, nil - } - switch k { - case "Method": - return res.Method, nil - case "URL": - return res.URL, nil - case "Header": - return res.Header, nil - case "Body": - return res.Body, nil - case "Form": - return res.Form, nil - case "PostForm": - return res.PostForm, nil - } - - return nil, fmt.Errorf("unknown field '%s'", k) -} - -// httpRequestTypedValueSelector is field accessor for *types.HttpRequest -func httpRequestTypedValueSelector(res *types.HttpRequest, k string) (TypedValue, error) { - if res == nil { - return nil, nil - } - switch k { - case "Method": - return NewString(res.Method) - case "URL": - return NewUrl(res.URL) - case "Header": - return NewKVV(res.Header) - case "Body": - return NewHttpRequestBody(res.Body) - case "Form": - return NewKVV(res.Form) - case "PostForm": - return NewKVV(res.PostForm) - } - - return nil, fmt.Errorf("unknown field '%s'", k) -} - -// assignToHttpRequest is field value setter for *types.HttpRequest -func assignToHttpRequest(res *types.HttpRequest, k string, val interface{}) error { - switch k { - case "Method": - aux, err := CastToString(val) - if err != nil { - return err - } - - res.Method = aux - return nil - case "URL": - aux, err := CastToUrl(val) - if err != nil { - return err - } - - res.URL = aux - return nil - case "Header": - aux, err := CastToKVV(val) - if err != nil { - return err - } - - res.Header = aux - return nil - case "Body": - aux, err := CastToHttpRequestBody(val) - if err != nil { - return err - } - - res.Body = aux - return nil - case "Form": - aux, err := CastToKVV(val) - if err != nil { - return err - } - - res.Form = aux - return nil - case "PostForm": - aux, err := CastToKVV(val) - if err != nil { - return err - } - - res.PostForm = aux - return nil - } - - return fmt.Errorf("unknown field '%s'", k) -} - -// HttpRequestBody is an expression type, wrapper for *types.HttpRequestBody type -type HttpRequestBody struct { - value *types.HttpRequestBody - mux sync.RWMutex -} - -// NewHttpRequestBody creates new instance of HttpRequestBody expression type -func NewHttpRequestBody(val interface{}) (*HttpRequestBody, error) { - if c, err := CastToHttpRequestBody(val); err != nil { - return nil, fmt.Errorf("unable to create HttpRequestBody: %w", err) - } else { - return &HttpRequestBody{value: c}, nil - } -} - -// Get return underlying value on HttpRequestBody -func (t *HttpRequestBody) Get() interface{} { - t.mux.RLock() - defer t.mux.RUnlock() - return t.value -} - -// GetValue returns underlying value on HttpRequestBody -func (t *HttpRequestBody) GetValue() *types.HttpRequestBody { - t.mux.RLock() - defer t.mux.RUnlock() - return t.value -} - -// Type return type name -func (HttpRequestBody) Type() string { return "HttpRequestBody" } - -// Cast converts value to *types.HttpRequestBody -func (HttpRequestBody) Cast(val interface{}) (TypedValue, error) { - return NewHttpRequestBody(val) -} - -// Assign new value to HttpRequestBody -// -// value is first passed through CastToHttpRequestBody -func (t *HttpRequestBody) Assign(val interface{}) error { - if c, err := CastToHttpRequestBody(val); err != nil { - return err - } else { - t.value = c - return nil - } -} - -func (t *HttpRequestBody) AssignFieldValue(key string, val TypedValue) error { - t.mux.Lock() - defer t.mux.Unlock() - return assignToHttpRequestBody(t.value, key, val) -} - -// SelectGVal implements gval.Selector requirements -// -// It allows gval lib to access HttpRequestBody's underlying value (*types.HttpRequestBody) -// and it's fields -// -func (t *HttpRequestBody) SelectGVal(ctx context.Context, k string) (interface{}, error) { - t.mux.RLock() - defer t.mux.RUnlock() - return httpRequestBodyGValSelector(t.value, k) -} - -// Select is field accessor for *types.HttpRequestBody -// -// Similar to SelectGVal but returns typed values -func (t *HttpRequestBody) Select(k string) (TypedValue, error) { - t.mux.RLock() - defer t.mux.RUnlock() - return httpRequestBodyTypedValueSelector(t.value, k) -} - -func (t *HttpRequestBody) Has(k string) bool { - t.mux.RLock() - defer t.mux.RUnlock() - switch k { - case "Body": - return true - case "Buffer": - return true - } - return false -} - -// httpRequestBodyGValSelector is field accessor for *types.HttpRequestBody -func httpRequestBodyGValSelector(res *types.HttpRequestBody, k string) (interface{}, error) { - if res == nil { - return nil, nil - } - switch k { - case "Body": - return res.Body, nil - case "Buffer": - return res.Buffer, nil - } - - return nil, fmt.Errorf("unknown field '%s'", k) -} - -// httpRequestBodyTypedValueSelector is field accessor for *types.HttpRequestBody -func httpRequestBodyTypedValueSelector(res *types.HttpRequestBody, k string) (TypedValue, error) { - if res == nil { - return nil, nil - } - switch k { - case "Body": - return NewReader(res.Body) - case "Buffer": - return NewBytes(res.Buffer) - } - - return nil, fmt.Errorf("unknown field '%s'", k) -} - -// assignToHttpRequestBody is field value setter for *types.HttpRequestBody -func assignToHttpRequestBody(res *types.HttpRequestBody, k string, val interface{}) error { - switch k { - case "Body": - aux, err := CastToReader(val) - if err != nil { - return err - } - - res.Body = aux - return nil - case "Buffer": - aux, err := CastToBytes(val) - if err != nil { - return err - } - - res.Buffer = aux - return nil - } - - return fmt.Errorf("unknown field '%s'", k) -} - -// Url is an expression type, wrapper for *types.Url type -type Url struct { - value *types.Url - mux sync.RWMutex -} - -// NewUrl creates new instance of Url expression type -func NewUrl(val interface{}) (*Url, error) { - if c, err := CastToUrl(val); err != nil { - return nil, fmt.Errorf("unable to create Url: %w", err) - } else { - return &Url{value: c}, nil - } -} - -// Get return underlying value on Url -func (t *Url) Get() interface{} { - t.mux.RLock() - defer t.mux.RUnlock() - return t.value -} - -// GetValue returns underlying value on Url -func (t *Url) GetValue() *types.Url { - t.mux.RLock() - defer t.mux.RUnlock() - return t.value -} - -// Type return type name -func (Url) Type() string { return "Url" } - -// Cast converts value to *types.Url -func (Url) Cast(val interface{}) (TypedValue, error) { - return NewUrl(val) -} - -// Assign new value to Url -// -// value is first passed through CastToUrl -func (t *Url) Assign(val interface{}) error { - if c, err := CastToUrl(val); err != nil { - return err - } else { - t.value = c - return nil - } -} - -func (t *Url) AssignFieldValue(key string, val TypedValue) error { - t.mux.Lock() - defer t.mux.Unlock() - return assignToUrl(t.value, key, val) -} - -// SelectGVal implements gval.Selector requirements -// -// It allows gval lib to access Url's underlying value (*types.Url) -// and it's fields -// -func (t *Url) SelectGVal(ctx context.Context, k string) (interface{}, error) { - t.mux.RLock() - defer t.mux.RUnlock() - return urlGValSelector(t.value, k) -} - -// Select is field accessor for *types.Url -// -// Similar to SelectGVal but returns typed values -func (t *Url) Select(k string) (TypedValue, error) { - t.mux.RLock() - defer t.mux.RUnlock() - return urlTypedValueSelector(t.value, k) -} - -func (t *Url) Has(k string) bool { - t.mux.RLock() - defer t.mux.RUnlock() - switch k { - case "Scheme": - return true - case "Opaque": - return true - case "Host": - return true - case "Path": - return true - case "RawPath": - return true - case "ForceQuery": - return true - case "RawQuery": - return true - case "Fragment": - return true - case "RawFragment": - return true - } - return false -} - -// urlGValSelector is field accessor for *types.Url -func urlGValSelector(res *types.Url, k string) (interface{}, error) { - if res == nil { - return nil, nil - } - switch k { - case "Scheme": - return res.Scheme, nil - case "Opaque": - return res.Opaque, nil - case "Host": - return res.Host, nil - case "Path": - return res.Path, nil - case "RawPath": - return res.RawPath, nil - case "ForceQuery": - return res.ForceQuery, nil - case "RawQuery": - return res.RawQuery, nil - case "Fragment": - return res.Fragment, nil - case "RawFragment": - return res.RawFragment, nil - } - - return nil, fmt.Errorf("unknown field '%s'", k) -} - -// urlTypedValueSelector is field accessor for *types.Url -func urlTypedValueSelector(res *types.Url, k string) (TypedValue, error) { - if res == nil { - return nil, nil - } - switch k { - case "Scheme": - return NewString(res.Scheme) - case "Opaque": - return NewString(res.Opaque) - case "Host": - return NewString(res.Host) - case "Path": - return NewString(res.Path) - case "RawPath": - return NewString(res.RawPath) - case "ForceQuery": - return NewBoolean(res.ForceQuery) - case "RawQuery": - return NewString(res.RawQuery) - case "Fragment": - return NewString(res.Fragment) - case "RawFragment": - return NewString(res.RawFragment) - } - - return nil, fmt.Errorf("unknown field '%s'", k) -} - -// assignToUrl is field value setter for *types.Url -func assignToUrl(res *types.Url, k string, val interface{}) error { - switch k { - case "Scheme": - aux, err := CastToString(val) - if err != nil { - return err - } - - res.Scheme = aux - return nil - case "Opaque": - aux, err := CastToString(val) - if err != nil { - return err - } - - res.Opaque = aux - return nil - case "Host": - aux, err := CastToString(val) - if err != nil { - return err - } - - res.Host = aux - return nil - case "Path": - aux, err := CastToString(val) - if err != nil { - return err - } - - res.Path = aux - return nil - case "RawPath": - aux, err := CastToString(val) - if err != nil { - return err - } - - res.RawPath = aux - return nil - case "ForceQuery": - aux, err := CastToBoolean(val) - if err != nil { - return err - } - - res.ForceQuery = aux - return nil - case "RawQuery": - aux, err := CastToString(val) - if err != nil { - return err - } - - res.RawQuery = aux - return nil - case "Fragment": - aux, err := CastToString(val) - if err != nil { - return err - } - - res.Fragment = aux - return nil - case "RawFragment": - aux, err := CastToString(val) - if err != nil { - return err - } - - res.RawFragment = aux - return nil - } - - return fmt.Errorf("unknown field '%s'", k) -} diff --git a/automation/automation/expr_types.go b/automation/automation/expr_types.go index 15cfe6cd7..96bf684f9 100644 --- a/automation/automation/expr_types.go +++ b/automation/automation/expr_types.go @@ -1,15 +1,11 @@ package automation import ( - "encoding/json" "fmt" "io" - "net/http" - "net/url" - "github.com/cortezaproject/corteza-server/automation/types" - atypes "github.com/cortezaproject/corteza-server/pkg/apigw/types" "github.com/cortezaproject/corteza-server/pkg/expr" + "github.com/cortezaproject/corteza-server/pkg/http" "gopkg.in/mail.v2" ) @@ -37,74 +33,6 @@ func CastToEmailMessage(val interface{}) (out *emailMessage, err error) { } } -func CastToHttpRequest(val interface{}) (out *types.HttpRequest, err error) { - switch val := val.(type) { - case expr.Iterator: - out = &types.HttpRequest{} - return out, val.Each(func(k string, v expr.TypedValue) error { - return assignToHttpRequest(out, k, v) - }) - } - - switch val := expr.UntypedValue(val).(type) { - case *http.Request: - rr := &types.HttpRequest{} - - assignToHttpRequest(rr, "Method", val.Method) - assignToHttpRequest(rr, "URL", val.URL) - assignToHttpRequest(rr, "Header", val.Header) - assignToHttpRequest(rr, "Body", val.Body) - assignToHttpRequest(rr, "Form", val.Form) - assignToHttpRequest(rr, "PostForm", val.PostForm) - - return rr, nil - case *types.HttpRequest: - return val, nil - case nil: - return &types.HttpRequest{}, nil - default: - return &types.HttpRequest{}, fmt.Errorf("unable to cast type %T to %T", val, out) - } -} - -func CastToHttpRequestBody(val interface{}) (out *types.HttpRequestBody, err error) { - switch val := val.(type) { - case io.ReadCloser: - rr := &types.HttpRequestBody{} - return rr, assignToHttpRequestBody(rr, "Body", val) - } - - switch val := expr.UntypedValue(val).(type) { - case *io.ReadCloser: - rr := &types.HttpRequestBody{} - return rr, assignToHttpRequestBody(rr, "Body", val) - case *types.HttpRequestBody: - return val, nil - case nil: - return &types.HttpRequestBody{}, nil - default: - return &types.HttpRequestBody{}, fmt.Errorf("unable to cast type %T to %T", val, out) - } -} - -func CastToUrl(val interface{}) (out *types.Url, err error) { - switch val := expr.UntypedValue(val).(type) { - case *url.URL: - u := &types.Url{} - - m, _ := json.Marshal(val) - _ = json.Unmarshal(m, u) - - return u, nil - case *types.Url: - return val, nil - case nil: - return &types.Url{}, nil - default: - return &types.Url{}, fmt.Errorf("unable to cast type %T to %T", val, out) - } -} - func ReadRequestBody(in interface{}) (s string) { var ( b []byte @@ -112,18 +40,8 @@ func ReadRequestBody(in interface{}) (s string) { ) switch val := in.(type) { - case *HttpRequest: - b, err = val.Get().(*types.HttpRequest).ReadBody() - case *HttpRequestBody: - b, err = val.Get().(*types.HttpRequestBody).Read() - case *atypes.HttpRequest: - b, err = val.ReadBody() - case *types.HttpRequest: - b, err = val.ReadBody() - case *atypes.HttpRequestBody: - b, err = val.Read() - case *types.HttpRequestBody: - b, err = val.Read() + case *http.Request: + b, err = io.ReadAll(val.Body) case io.Reader: b, err = io.ReadAll(val) default: diff --git a/automation/automation/expr_types.yaml b/automation/automation/expr_types.yaml index 131cc6841..2ad6f72c1 100644 --- a/automation/automation/expr_types.yaml +++ b/automation/automation/expr_types.yaml @@ -1,8 +1,5 @@ package: automation -imports: - - github.com/cortezaproject/corteza-server/automation/types - types: EmailMessage: # using ad-hoc type for now, we'll port this to something internal @@ -19,31 +16,4 @@ types: # - { name: 'parts', exprType: 'Any', goType: 'map[string]io.Reader', mode: ro } # - { name: 'embedded', exprType: 'Any', goType: 'map[string]io.Reader', mode: ro } # - { name: 'attachments', exprType: 'Any', goType: 'map[string]io.Reader', mode: ro } - HttpRequest: - as: '*types.HttpRequest' - struct: - - { name: 'Method', exprType: 'String', goType: 'string' } - - { name: 'URL', exprType: 'Url', goType: 'url.Url' } - - { name: 'Header', exprType: 'KVV', goType: 'map[string][]string' } - - { name: 'Body', exprType: 'HttpRequestBody', goType: '*types.HttpRequestBody' } - - { name: 'Form', exprType: 'KVV', goType: 'map[string][]string' } - - { name: 'PostForm', exprType: 'KVV', goType: 'map[string][]string' } - - HttpRequestBody: - as: '*types.HttpRequestBody' - struct: - - { name: 'Body', exprType: 'Reader', goType: 'io.Reader' } - - { name: 'Buffer', exprType: 'Bytes', goType: '[]byte' } - - Url: - as: '*types.Url' - struct: - - { name: 'Scheme', exprType: 'String', goType: 'string' } - - { name: 'Opaque', exprType: 'String', goType: 'string' } - - { name: 'Host', exprType: 'String', goType: 'string' } - - { name: 'Path', exprType: 'String', goType: 'string' } - - { name: 'RawPath', exprType: 'String', goType: 'string' } - - { name: 'ForceQuery', exprType: 'Boolean', goType: 'bool' } - - { name: 'RawQuery', exprType: 'String', goType: 'string' } - - { name: 'Fragment', exprType: 'String', goType: 'string' } - - { name: 'RawFragment', exprType: 'String', goType: 'string' } + \ No newline at end of file diff --git a/automation/automation/jsenv_handler.go b/automation/automation/jsenv_handler.go index 3a72d64d1..184decf0f 100644 --- a/automation/automation/jsenv_handler.go +++ b/automation/automation/jsenv_handler.go @@ -63,8 +63,6 @@ func (h jsenvHandler) execute(ctx context.Context, args *jsenvExecuteArgs) (res } switch vv := out.(type) { - case uint64: - res.ResultInt = int64(vv) case int64: res.ResultInt = int64(vv) case string: diff --git a/automation/automation/jsenv_handler_test.go b/automation/automation/jsenv_handler_test.go new file mode 100644 index 000000000..5ffe1e3a7 --- /dev/null +++ b/automation/automation/jsenv_handler_test.go @@ -0,0 +1,201 @@ +package automation + +import ( + "context" + "errors" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/cortezaproject/corteza-server/pkg/expr" + h "github.com/cortezaproject/corteza-server/pkg/http" + "github.com/stretchr/testify/require" +) + +func Test_jsenvHandler(t *testing.T) { + type ( + exp struct { + s string + i int64 + a interface{} + } + + tf struct { + name string + exp *exp + err error + params *jsenvExecuteArgs + } + ) + + var ( + handler = &jsenvHandler{} + tcc = []tf{ + { + name: "jsenv handler check payload", + err: errors.New(`could not process payload, scope missing`), + params: &jsenvExecuteArgs{ + hasScope: false, + hasSource: true, + }, + }, + { + name: "jsenv handler check payload", + err: errors.New(`could not process payload, function missing`), + params: &jsenvExecuteArgs{ + hasScope: true, + hasSource: false, + }, + }, + { + name: "jsenv handler invalid function", + err: errors.New(`could not register jsenv function: SyntaxError: SyntaxError: (anonymous): Line 1:74 Unexpected token function (and 1 more errors)`), + params: &jsenvExecuteArgs{ + hasScope: true, + hasSource: true, + Source: `invalid function here...`, + }, + }, + { + name: "jsenv handler check payload", + err: errors.New(``), + params: &jsenvExecuteArgs{ + hasScope: true, + hasSource: true, + Source: `return nonexistent`, + }, + }, + { + name: "jsenv handler parse request body json to string", + exp: &exp{ + s: `bar`, + i: 0, + }, + params: &jsenvExecuteArgs{ + hasScope: true, + hasSource: true, + Scope: mustAny(expr.NewAny(makeRequest(t, `{"foo":"bar"}`))), + Source: `const b = JSON.parse(readRequestBody(input)); return b.foo;`, + }, + }, + { + name: "jsenv handler parse request body json to int", + exp: &exp{ + s: ``, + i: 42, + }, + params: &jsenvExecuteArgs{ + hasScope: true, + hasSource: true, + Scope: mustAny(expr.NewAny(makeRequest(t, `{"foo":42}`))), + Source: `const b = JSON.parse(readRequestBody(input)); return b.foo;`, + }, + }, + { + name: "jsenv handler parse request body json to int", + exp: &exp{ + s: ``, + i: 42, + }, + params: &jsenvExecuteArgs{ + hasScope: true, + hasSource: true, + Scope: mustAny(expr.NewAny(makeRequest(t, `42`))), + Source: `const b = readRequestBody(input); return parseInt(b);`, + }, + }, + { + name: "jsenv handler parse request body try catch", + exp: &exp{ + s: `caught`, + i: 0, + }, + params: &jsenvExecuteArgs{ + hasScope: true, + hasSource: true, + Scope: mustAny(expr.NewAny(makeRequest(t, `42`))), + Source: `try { const b = readRequestBody(input_NONEXISTENT); } catch (e) { return 'caught'; }`, + }, + }, + { + name: "jsenv handler parse request body json to float", + exp: &exp{ + s: ``, + i: 0, + a: 42.690, + }, + params: &jsenvExecuteArgs{ + hasScope: true, + hasSource: true, + Scope: mustAny(expr.NewAny(makeRequest(t, `42.690`))), + Source: `const b = readRequestBody(input); return parseFloat(b);`, + }, + }, + { + name: "jsenv handler input scope", + exp: &exp{ + s: ``, + i: 41, + }, + params: &jsenvExecuteArgs{ + hasScope: true, + hasSource: true, + Scope: mustAny(expr.NewAny(makeRequest(t, `42`))), + Source: `const b = readRequestBody(input); return b - 1;`, + }, + }, + } + ) + + for _, tc := range tcc { + t.Run(tc.name, func(t *testing.T) { + var ( + req = require.New(t) + ctx = context.Background() + ) + + handler.preloadVm() + + out, err := handler.execute(ctx, tc.params) + + if tc.err == nil { + req.NoError(err) + } else { + req.Error(err) + } + + if tc.exp != nil { + req.Equal(tc.exp.s, out.ResultString) + req.Equal(tc.exp.i, out.ResultInt) + + if tc.exp.a != nil { + req.Equal(tc.exp.a, out.ResultAny) + } + } + }) + } +} + +func makeRequest(t *testing.T, b string) *h.Request { + r, err := http.NewRequest("POST", "/foo", ioutil.NopCloser(strings.NewReader(b))) + + if err != nil { + t.Error(err) + } + + ar, err := h.NewRequest(r) + + if err != nil { + t.Error(err) + } + + return ar +} + +func mustAny(v *expr.Any, err error) *expr.Any { + if err != nil { + panic(err) + } + return v +} diff --git a/automation/service/service.go b/automation/service/service.go index 184eea406..8838583b9 100644 --- a/automation/service/service.go +++ b/automation/service/service.go @@ -113,10 +113,9 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, ws websock &expr.KVV{}, &expr.Reader{}, &expr.Vars{}, + &expr.HttpRequest{}, &automation.EmailMessage{}, - &automation.HttpRequest{}, - &automation.HttpRequestBody{}, ) automation.HttpRequestHandler(Registry()) diff --git a/pkg/apigw/filter/postfilter.go b/pkg/apigw/filter/postfilter.go index 6993c23e7..8710f4681 100644 --- a/pkg/apigw/filter/postfilter.go +++ b/pkg/apigw/filter/postfilter.go @@ -7,7 +7,6 @@ import ( "net/http" "net/url" - "github.com/cortezaproject/corteza-server/automation/service" atypes "github.com/cortezaproject/corteza-server/automation/types" agctx "github.com/cortezaproject/corteza-server/pkg/apigw/ctx" "github.com/cortezaproject/corteza-server/pkg/apigw/types" @@ -194,7 +193,7 @@ func NewJsonResponse(reg typesRegistry) (e *jsonResponse) { } func (j jsonResponse) New() types.Handler { - return NewJsonResponse(service.Registry()) + return NewJsonResponse(j.reg) } func (j jsonResponse) String() string { diff --git a/pkg/apigw/filter/postfilter_test.go b/pkg/apigw/filter/postfilter_test.go index 6e599e17f..a8753e7e7 100644 --- a/pkg/apigw/filter/postfilter_test.go +++ b/pkg/apigw/filter/postfilter_test.go @@ -7,13 +7,16 @@ import ( "strings" "testing" - "github.com/cortezaproject/corteza-server/automation/service" agctx "github.com/cortezaproject/corteza-server/pkg/apigw/ctx" "github.com/cortezaproject/corteza-server/pkg/apigw/types" "github.com/cortezaproject/corteza-server/pkg/expr" "github.com/stretchr/testify/require" ) +type ( + mockHandlerRegistry struct{} +) + func Test_redirectionMerge(t *testing.T) { var ( tcc = []tf{ @@ -141,7 +144,7 @@ func Test_jsonResponse(t *testing.T) { r = r.WithContext(agctx.ScopeToContext(context.Background(), scope)) - h := getHandler(NewJsonResponse(service.Registry())) + h := getHandler(NewJsonResponse(&mockHandlerRegistry{})) h, err := h.Merge([]byte(tc.expr)) req.NoError(err) @@ -164,3 +167,7 @@ func Test_jsonResponse(t *testing.T) { func getHandler(h types.Handler) types.Handler { return h } + +func (r *mockHandlerRegistry) Type(ref string) expr.Type { + return expr.Any{} +} diff --git a/pkg/apigw/filter/processer.go b/pkg/apigw/filter/processer.go index 64e98c338..1a48f2e71 100644 --- a/pkg/apigw/filter/processer.go +++ b/pkg/apigw/filter/processer.go @@ -6,9 +6,9 @@ import ( "encoding/json" "errors" "fmt" - "io" "net/http" + "github.com/cortezaproject/corteza-server/automation/automation" atypes "github.com/cortezaproject/corteza-server/automation/types" agctx "github.com/cortezaproject/corteza-server/pkg/apigw/ctx" "github.com/cortezaproject/corteza-server/pkg/apigw/types" @@ -29,6 +29,7 @@ type ( } WfExecer interface { + Load(ctx context.Context) error Exec(ctx context.Context, workflowID uint64, p atypes.WorkflowExecParams) (*expr.Vars, atypes.Stacktrace, error) } @@ -36,6 +37,7 @@ type ( types.FilterMeta vm jsenv.Vm + fn *jsenv.Fn log *zap.Logger params struct { @@ -80,7 +82,12 @@ func (h workflow) Meta() types.FilterMeta { func (h *workflow) Merge(params []byte) (types.Handler, error) { err := json.NewDecoder(bytes.NewBuffer(params)).Decode(&h.params) - return h, err + if err != nil { + return h, err + } + + // preload workflow cache + return h, h.d.Load(context.Background()) } func (h workflow) Handler() types.HandlerFunc { @@ -91,21 +98,10 @@ func (h workflow) Handler() types.HandlerFunc { scope = agctx.ScopeFromContext(ctx) ) - // original request with body as io.Reader - // read-only - ar, err := scope.Get("request") + // cleanup scope for wf + scp := filterScope(scope, "opts") - if err != nil { - return err - } - - // setup scope for workflow - vv := map[string]interface{}{ - "request": ar, - } - - // get the request data and put it into vars - in, err := expr.NewVars(vv) + in, err := expr.NewVars(scp.Dict()) if err != nil { return pe.Internal("could not validate request data: %v", err) @@ -143,17 +139,10 @@ func (h workflow) Handler() types.HandlerFunc { scope.Set(k, v) } - ss := scope.Filter(func(k string, v interface{}) bool { - if k == "eventType" || k == "resourceType" { - return false - } + scope = filterScope(scope, "eventType", "resourceType", "invoker") - return true - }) - - scope = ss - - scope.Set("request", ar) + // update scope for next items in pipeline + r.WithContext(agctx.ScopeToContext(ctx, scope)) return nil } @@ -162,7 +151,6 @@ func (h workflow) Handler() types.HandlerFunc { func NewPayload(l *zap.Logger) (p *processerPayload) { p = &processerPayload{} - // todo - check the consequences of doing this here p.vm = jsenv.New(jsenv.NewTransformer(jsenv.LoaderJS, jsenv.TargetES2016)) p.log = l @@ -179,11 +167,7 @@ func NewPayload(l *zap.Logger) (p *processerPayload) { } // register a request body reader - // since it's a readcloser, it can be read only once - p.vm.Register("readRequestBody", func(rc io.ReadCloser) string { - b, _ := io.ReadAll(rc) - return string(b) - }) + p.vm.Register("readRequestBody", automation.ReadRequestBody) return } @@ -211,6 +195,8 @@ func (h *processerPayload) Merge(params []byte) (types.Handler, error) { return nil, errors.New("could not register function, body empty") } + h.fn, _ = h.vm.RegisterFunction(h.params.Func) + return h, err } @@ -221,15 +207,18 @@ func (h processerPayload) Handler() types.HandlerFunc { scope = agctx.ScopeFromContext(ctx) ) - scope.Set("request", r) - - fn, err := h.vm.RegisterFunction(h.params.Func) + // cleanup scope for js + scp := filterScope(scope, "opts") + // check fn type if err != nil { return pe.InvalidData("could not register function: %v", err) } - out, err := fn.Exec(h.vm.New(scope)) + // need to find a consistent approach to the workflow jsenv function + // wf: input expr Var and the resulting variable in the jsenv is `input` + // apigw: input types.Scope and the resulting variable in the jsenv is `input['some_var']` + out, err := h.fn.Exec(h.vm.New(scp)) if err != nil { return pe.Internal("could not exec payload function: %v", err) @@ -240,7 +229,7 @@ func (h processerPayload) Handler() types.HandlerFunc { // check if string switch out.(type) { - case string: + case string, []byte: // handling the newline, to keep the consistency with the json encoder // which automatically appends the newline _, err = rw.Write([]byte(fmt.Sprintf("%s\n", out))) @@ -259,3 +248,17 @@ func (h processerPayload) Handler() types.HandlerFunc { func (h processerPayload) VM() jsenv.Vm { return h.vm } + +func filterScope(scope *types.Scp, kk ...string) (s *types.Scp) { + s = scope.Filter(func(k string, v interface{}) bool { + for _, v := range kk { + if k == v { + return false + } + } + + return true + }) + + return +} diff --git a/pkg/apigw/filter/processer_test.go b/pkg/apigw/filter/processer_test.go index 2b3f73925..fed3dfd29 100644 --- a/pkg/apigw/filter/processer_test.go +++ b/pkg/apigw/filter/processer_test.go @@ -3,19 +3,108 @@ package filter import ( "context" "encoding/json" + "errors" "io/ioutil" "net/http" "net/http/httptest" "strings" "testing" + atypes "github.com/cortezaproject/corteza-server/automation/types" agctx "github.com/cortezaproject/corteza-server/pkg/apigw/ctx" "github.com/cortezaproject/corteza-server/pkg/apigw/types" + "github.com/cortezaproject/corteza-server/pkg/expr" + h "github.com/cortezaproject/corteza-server/pkg/http" "github.com/cortezaproject/corteza-server/pkg/options" + "github.com/cortezaproject/corteza-server/pkg/wfexec" "github.com/stretchr/testify/require" "go.uber.org/zap" ) +type ( + wfServicer struct { + load func(ctx context.Context) error + exec func(ctx context.Context, workflowID uint64, p atypes.WorkflowExecParams) (*expr.Vars, atypes.Stacktrace, error) + } +) + +func Test_processerWorkflow(t *testing.T) { + type ( + tf struct { + name string + err string + params string + wfs wfServicer + exp []string + } + ) + + var ( + tcc = []tf{ + { + name: "workflow processer", + exp: []string{"opts", "request"}, + params: `{"workflow":"1"}`, + err: `could not exec workflow: mocked error`, + wfs: wfServicer{ + load: func(ctx context.Context) error { + return nil + }, + exec: func(ctx context.Context, workflowID uint64, p atypes.WorkflowExecParams) (*expr.Vars, atypes.Stacktrace, error) { + return must(expr.NewVars(map[string]interface{}{"foo": "bar"})), make([]*wfexec.Frame, 0), errors.New("mocked error") + }, + }, + }, + { + name: "workflow processer", + exp: []string{"foo", "request", "opts"}, + params: `{"workflow":"1"}`, + wfs: wfServicer{ + load: func(ctx context.Context) error { + return nil + }, + exec: func(ctx context.Context, workflowID uint64, p atypes.WorkflowExecParams) (*expr.Vars, atypes.Stacktrace, error) { + return must(expr.NewVars(map[string]interface{}{"foo": "bar"})), make([]*wfexec.Frame, 0), nil + }, + }, + }, + } + ) + + for _, tc := range tcc { + t.Run(tc.name, func(t *testing.T) { + var ( + req = require.New(t) + rc = httptest.NewRecorder() + rq, _ = http.NewRequest("POST", "/foo", http.NoBody) + ar, err = h.NewRequest(rq) + pp = NewWorkflow(tc.wfs) + ) + + _, err = pp.Merge([]byte(tc.params)) + req.NoError(err) + + scope := &types.Scp{ + "opts": options.Apigw(), + "request": ar, + } + + rq = rq.WithContext(agctx.ScopeToContext(context.Background(), scope)) + + hn := pp.Handler() + err = hn(rc, rq) + + if tc.err != "" { + req.EqualError(err, tc.err) + } else { + req.NoError(err) + } + + req.ElementsMatch(tc.exp, agctx.ScopeFromContext(rq.Context()).Keys()) + }) + } +} + func Test_processerPayload(t *testing.T) { type ( tf struct { @@ -31,26 +120,26 @@ func Test_processerPayload(t *testing.T) { var ( tcc = []tf{ { - name: "payload processer", + name: "payload processer parse request body", rq: &http.Request{ Method: "POST", Body: ioutil.NopCloser(strings.NewReader(`[1,2,3]`)), }, exp: "2\n", params: prepareFuncPayload(t, ` - var b = JSON.parse(readRequestBody(input.Get('request').Body)); + const b = JSON.parse(readRequestBody(input.Get('request'))); return b[1]; `), }, { - name: "payload processer js map", + name: "payload processer js map request body", rq: &http.Request{ Method: "POST", Body: ioutil.NopCloser(strings.NewReader(`[{"name":"johnny", "surname":"mnemonic"},{"name":"johnny", "surname":"knoxville"}]`)), }, exp: "{\"count\":2,\"results\":[{\"fullname\":\"Johnny Mnemonic\"},{\"fullname\":\"Johnny Knoxville\"}]}\n", params: prepareFuncPayload(t, ` - var b = JSON.parse(readRequestBody(input.Get('request').Body)); + const b = JSON.parse(readRequestBody(input.Get('request'))); return { "results": @@ -78,12 +167,13 @@ func Test_processerPayload(t *testing.T) { for _, tc := range tcc { t.Run(tc.name, func(t *testing.T) { var ( - req = require.New(t) - rc = httptest.NewRecorder() + req = require.New(t) + rc = httptest.NewRecorder() + ar, err = h.NewRequest(tc.rq) ) pp := NewPayload(zap.NewNop()) - _, err := pp.Merge([]byte(tc.params)) + _, err = pp.Merge([]byte(tc.params)) if tc.errv != "" { req.EqualError(err, tc.errv) @@ -93,7 +183,8 @@ func Test_processerPayload(t *testing.T) { } scope := &types.Scp{ - "opts": options.Apigw(), + "opts": options.Apigw(), + "request": ar, } tc.rq = tc.rq.WithContext(agctx.ScopeToContext(context.Background(), scope)) @@ -118,3 +209,19 @@ func prepareFuncPayload(t *testing.T, s string) string { } return string(aux) } + +func (f wfServicer) Load(ctx context.Context) error { + return f.load(ctx) +} + +func (f wfServicer) Exec(ctx context.Context, workflowID uint64, p atypes.WorkflowExecParams) (*expr.Vars, atypes.Stacktrace, error) { + return f.exec(ctx, workflowID, p) +} + +func must(v *expr.Vars, err error) *expr.Vars { + if err != nil { + return nil + } + + return v +} diff --git a/pkg/apigw/registry/registry.go b/pkg/apigw/registry/registry.go index 9cdf1ac45..6a6f2e7dd 100644 --- a/pkg/apigw/registry/registry.go +++ b/pkg/apigw/registry/registry.go @@ -8,8 +8,6 @@ import ( "github.com/cortezaproject/corteza-server/pkg/apigw/filter" "github.com/cortezaproject/corteza-server/pkg/apigw/filter/proxy" "github.com/cortezaproject/corteza-server/pkg/apigw/types" - "github.com/cortezaproject/corteza-server/pkg/logger" - "github.com/cortezaproject/corteza-server/pkg/options" ) type ( @@ -74,7 +72,5 @@ func (r *Registry) Preload() { } func NewWorkflow() (wf filter.WfExecer) { - // implementation assumes that Corredor & Workflow options can not be changed - // in the run-time. - return service.Workflow(logger.Default().Named("workflow"), *options.Corredor(), *options.Workflow()) + return service.DefaultWorkflow } diff --git a/pkg/apigw/route.go b/pkg/apigw/route.go index 7fcb50300..4b2d76dbb 100644 --- a/pkg/apigw/route.go +++ b/pkg/apigw/route.go @@ -8,8 +8,8 @@ import ( actx "github.com/cortezaproject/corteza-server/pkg/apigw/ctx" "github.com/cortezaproject/corteza-server/pkg/apigw/types" "github.com/cortezaproject/corteza-server/pkg/auth" + h "github.com/cortezaproject/corteza-server/pkg/http" "github.com/cortezaproject/corteza-server/pkg/options" - "github.com/cortezaproject/corteza-server/system/automation" "go.uber.org/zap" ) @@ -42,12 +42,10 @@ func (r route) ServeHTTP(w http.ResponseWriter, req *http.Request) { r.log.Debug("started serving route") - // create a new automation HttpRequest - ar, err := automation.NewHttpRequest(req) + ar, err := h.NewRequest(req) if err != nil { - r.log.Error("could not prepare a request holder", zap.Error(err)) - return + r.log.Error("could not get initial request", zap.Error(err)) } scope.Set("opts", r.opts) diff --git a/pkg/expr/expr_types.gen.go b/pkg/expr/expr_types.gen.go index 64b8df24d..20dde2587 100644 --- a/pkg/expr/expr_types.gen.go +++ b/pkg/expr/expr_types.gen.go @@ -11,7 +11,9 @@ package expr import ( "context" "fmt" + "github.com/cortezaproject/corteza-server/pkg/http" "io" + "net/url" "sync" "time" ) @@ -464,6 +466,203 @@ func (t Handle) Compare(to TypedValue) (int, error) { } } +// HttpRequest is an expression type, wrapper for *http.Request type +type HttpRequest struct { + value *http.Request + mux sync.RWMutex +} + +// NewHttpRequest creates new instance of HttpRequest expression type +func NewHttpRequest(val interface{}) (*HttpRequest, error) { + if c, err := CastToHttpRequest(val); err != nil { + return nil, fmt.Errorf("unable to create HttpRequest: %w", err) + } else { + return &HttpRequest{value: c}, nil + } +} + +// Get return underlying value on HttpRequest +func (t *HttpRequest) Get() interface{} { + t.mux.RLock() + defer t.mux.RUnlock() + return t.value +} + +// GetValue returns underlying value on HttpRequest +func (t *HttpRequest) GetValue() *http.Request { + t.mux.RLock() + defer t.mux.RUnlock() + return t.value +} + +// Type return type name +func (HttpRequest) Type() string { return "HttpRequest" } + +// Cast converts value to *http.Request +func (HttpRequest) Cast(val interface{}) (TypedValue, error) { + return NewHttpRequest(val) +} + +// Assign new value to HttpRequest +// +// value is first passed through CastToHttpRequest +func (t *HttpRequest) Assign(val interface{}) error { + if c, err := CastToHttpRequest(val); err != nil { + return err + } else { + t.value = c + return nil + } +} + +func (t *HttpRequest) AssignFieldValue(key string, val TypedValue) error { + t.mux.Lock() + defer t.mux.Unlock() + return assignToHttpRequest(t.value, key, val) +} + +// SelectGVal implements gval.Selector requirements +// +// It allows gval lib to access HttpRequest's underlying value (*http.Request) +// and it's fields +// +func (t *HttpRequest) SelectGVal(ctx context.Context, k string) (interface{}, error) { + t.mux.RLock() + defer t.mux.RUnlock() + return httpRequestGValSelector(t.value, k) +} + +// Select is field accessor for *http.Request +// +// Similar to SelectGVal but returns typed values +func (t *HttpRequest) Select(k string) (TypedValue, error) { + t.mux.RLock() + defer t.mux.RUnlock() + return httpRequestTypedValueSelector(t.value, k) +} + +func (t *HttpRequest) Has(k string) bool { + t.mux.RLock() + defer t.mux.RUnlock() + switch k { + case "Method": + return true + case "URL": + return true + case "Header": + return true + case "Body": + return true + case "Form": + return true + case "PostForm": + return true + } + return false +} + +// httpRequestGValSelector is field accessor for *http.Request +func httpRequestGValSelector(res *http.Request, k string) (interface{}, error) { + if res == nil { + return nil, nil + } + switch k { + case "Method": + return res.Method, nil + case "URL": + return res.URL, nil + case "Header": + return res.Header, nil + case "Body": + return res.Body, nil + case "Form": + return res.Form, nil + case "PostForm": + return res.PostForm, nil + } + + return nil, fmt.Errorf("unknown field '%s'", k) +} + +// httpRequestTypedValueSelector is field accessor for *http.Request +func httpRequestTypedValueSelector(res *http.Request, k string) (TypedValue, error) { + if res == nil { + return nil, nil + } + switch k { + case "Method": + return NewString(res.Method) + case "URL": + return NewUrl(res.URL) + case "Header": + return NewKVV(res.Header) + case "Body": + return NewReader(res.Body) + case "Form": + return NewKVV(res.Form) + case "PostForm": + return NewKVV(res.PostForm) + } + + return nil, fmt.Errorf("unknown field '%s'", k) +} + +// assignToHttpRequest is field value setter for *http.Request +func assignToHttpRequest(res *http.Request, k string, val interface{}) error { + switch k { + case "Method": + aux, err := CastToString(val) + if err != nil { + return err + } + + res.Method = aux + return nil + case "URL": + aux, err := CastToUrl(val) + if err != nil { + return err + } + + res.URL = aux + return nil + case "Header": + aux, err := CastToKVV(val) + if err != nil { + return err + } + + res.Header = aux + return nil + case "Body": + aux, err := CastToReader(val) + if err != nil { + return err + } + + res.Body = aux + return nil + case "Form": + aux, err := CastToKVV(val) + if err != nil { + return err + } + + res.Form = aux + return nil + case "PostForm": + aux, err := CastToKVV(val) + if err != nil { + return err + } + + res.PostForm = aux + return nil + } + + return fmt.Errorf("unknown field '%s'", k) +} + // ID is an expression type, wrapper for uint64 type type ID struct { value uint64 @@ -883,6 +1082,55 @@ func (t UnsignedInteger) Compare(to TypedValue) (int, error) { } } +// Url is an expression type, wrapper for *url.URL type +type Url struct { + value *url.URL + mux sync.RWMutex +} + +// NewUrl creates new instance of Url expression type +func NewUrl(val interface{}) (*Url, error) { + if c, err := CastToUrl(val); err != nil { + return nil, fmt.Errorf("unable to create Url: %w", err) + } else { + return &Url{value: c}, nil + } +} + +// Get return underlying value on Url +func (t *Url) Get() interface{} { + t.mux.RLock() + defer t.mux.RUnlock() + return t.value +} + +// GetValue returns underlying value on Url +func (t *Url) GetValue() *url.URL { + t.mux.RLock() + defer t.mux.RUnlock() + return t.value +} + +// Type return type name +func (Url) Type() string { return "Url" } + +// Cast converts value to *url.URL +func (Url) Cast(val interface{}) (TypedValue, error) { + return NewUrl(val) +} + +// Assign new value to Url +// +// value is first passed through CastToUrl +func (t *Url) Assign(val interface{}) error { + if c, err := CastToUrl(val); err != nil { + return err + } else { + t.value = c + return nil + } +} + // Vars is an expression type, wrapper for map[string]TypedValue type type Vars struct { value map[string]TypedValue diff --git a/pkg/expr/expr_types.go b/pkg/expr/expr_types.go index 592f8baae..8802def14 100644 --- a/pkg/expr/expr_types.go +++ b/pkg/expr/expr_types.go @@ -16,6 +16,7 @@ import ( "github.com/PaesslerAG/gval" "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/pkg/handle" + h "github.com/cortezaproject/corteza-server/pkg/http" "github.com/spf13/cast" ) @@ -654,6 +655,36 @@ func CastToReader(val interface{}) (out io.Reader, err error) { } } +func CastToHttpRequest(val interface{}) (out *h.Request, err error) { + switch val := val.(type) { + case Iterator: + out = &h.Request{} + return out, val.Each(func(k string, v TypedValue) error { + return assignToHttpRequest(out, k, v) + }) + } + + switch val := UntypedValue(val).(type) { + case *h.Request: + return val, nil + case nil: + return &h.Request{}, nil + default: + return &h.Request{}, fmt.Errorf("unable to cast type %T to %T", val, out) + } +} + +func CastToUrl(val interface{}) (out *url.URL, err error) { + switch val := UntypedValue(val).(type) { + case *url.URL: + return val, nil + case nil: + return &url.URL{}, nil + default: + return &url.URL{}, fmt.Errorf("unable to cast type %T to %T", val, out) + } +} + func (t *KVV) Each(fn func(k string, v TypedValue) error) (err error) { if t == nil || t.value == nil { return diff --git a/pkg/expr/expr_types.yaml b/pkg/expr/expr_types.yaml index f5d6cbfa8..7b11a81e4 100644 --- a/pkg/expr/expr_types.yaml +++ b/pkg/expr/expr_types.yaml @@ -2,6 +2,8 @@ package: expr imports: - io - time + - github.com/cortezaproject/corteza-server/pkg/http + - net/url types: Any: @@ -76,3 +78,15 @@ types: Reader: as: 'io.Reader' + HttpRequest: + as: '*http.Request' + struct: + - { name: 'Method', exprType: 'String', goType: 'string' } + - { name: 'URL', exprType: 'Url', goType: 'url.Url' } + - { name: 'Header', exprType: 'KVV', goType: 'map[string][]string' } + - { name: 'Body', exprType: 'Reader', goType: '*types.BufferedReader' } + - { name: 'Form', exprType: 'KVV', goType: 'map[string][]string' } + - { name: 'PostForm', exprType: 'KVV', goType: 'map[string][]string' } + + Url: + as: '*url.URL' diff --git a/pkg/expr/vars.go b/pkg/expr/vars.go index 6ce2d03dd..a6c13f6a9 100644 --- a/pkg/expr/vars.go +++ b/pkg/expr/vars.go @@ -5,7 +5,6 @@ import ( "database/sql/driver" "encoding/json" "fmt" - "net/http" "reflect" "strings" @@ -363,37 +362,10 @@ func (t *Vars) MarshalJSON() ([]byte, error) { aux[k] = &typedValueWrap{Type: v.Type()} - rv := v.Get() - // @todo this is a temporary solution. - // The JSON marshling failed due to some receiver functions on the - // HTTP request struct. - if hv, ok := rv.(*http.Request); ok { - aux[k].Value = map[string]interface{}{ - "Method": hv.Method, - "URL": hv.URL, - "Proto": hv.Proto, - "ProtoMajor": hv.ProtoMajor, - "ProtoMinor": hv.ProtoMinor, - "Header": hv.Header, - "ContentLength": hv.ContentLength, - "TransferEncoding": hv.TransferEncoding, - "Close": hv.Close, - "Host": hv.Host, - "Form": hv.Form, - "PostForm": hv.PostForm, - "MultipartForm": hv.MultipartForm, - "Trailer": hv.Trailer, - "RemoteAddr": hv.RemoteAddr, - "RequestURI": hv.RequestURI, - "TLS": hv.TLS, - "Response": hv.Response, - } + if _, is := v.(json.Marshaler); is { + aux[k].Value = v } else { - if _, is := v.(json.Marshaler); is { - aux[k].Value = v - } else { - aux[k].Value = v.Get() - } + aux[k].Value = v.Get() } } diff --git a/pkg/http/client.go b/pkg/http/client.go index 563634554..2055c96de 100644 --- a/pkg/http/client.go +++ b/pkg/http/client.go @@ -28,8 +28,6 @@ type ( config *Config } - Request http.Request - DebugLevel string ) diff --git a/pkg/http/request.go b/pkg/http/request.go new file mode 100644 index 000000000..37cdf6d59 --- /dev/null +++ b/pkg/http/request.go @@ -0,0 +1,91 @@ +package http + +import ( + "encoding/json" + "io" + "mime/multipart" + "net/http" + "net/url" +) + +type ( + Request struct { + *http.Request + Body io.Reader + } + + BufferedReader struct { + buffer []byte + } +) + +// NewBufferedReader creates a new reader from readcloser +func NewBufferedReader(r io.ReadCloser) (b *BufferedReader, err error) { + var bb []byte + + if bb, err = io.ReadAll(r); err != nil { + return + } + + b = &BufferedReader{bb} + return +} + +// NewRequest creates a new Request with the buffered ready body +func NewRequest(r *http.Request) (rr *Request, err error) { + rs, err := NewBufferedReader(r.Body) + + if err != nil { + return + } + + rr = &Request{r, rs} + return +} + +func (bb *BufferedReader) Read(p []byte) (n int, err error) { + if len(bb.buffer) <= n { + err = io.EOF + return + } + + if c := cap(p); c > 0 { + for n < c { + if len(bb.buffer) <= n { + err = io.EOF + break + } + + p[n] = bb.buffer[n] + n++ + } + } + + return +} + +func (bb *Request) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Method string + URL *url.URL + Header http.Header + ContentLength int64 + Host string + Form url.Values + PostForm url.Values + MultipartForm *multipart.Form + RemoteAddr string + RequestURI string + }{ + Method: bb.Method, + URL: bb.URL, + Header: bb.Header, + ContentLength: bb.ContentLength, + Host: bb.Host, + Form: bb.Form, + PostForm: bb.PostForm, + MultipartForm: bb.MultipartForm, + RemoteAddr: bb.RemoteAddr, + RequestURI: bb.RequestURI, + }) +} diff --git a/pkg/http/request_test.go b/pkg/http/request_test.go new file mode 100644 index 000000000..952fd764e --- /dev/null +++ b/pkg/http/request_test.go @@ -0,0 +1,40 @@ +package http + +import ( + "io" + h "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_requestReadMultiple(t *testing.T) { + var req = require.New(t) + r, _ := h.NewRequest("POST", "/foo", strings.NewReader(`foo body`)) + + rs, err := NewBufferedReader(r.Body) + + req.NoError(err) + req.Equal(`foo body`, must(io.ReadAll(rs))) + req.Equal(`foo body`, must(io.ReadAll(rs))) +} + +func Test_requestReadMultipleNoBody(t *testing.T) { + var req = require.New(t) + r, _ := h.NewRequest("POST", "/foo", h.NoBody) + + rs, err := NewBufferedReader(r.Body) + + req.NoError(err) + req.Equal(``, must(io.ReadAll(rs))) + req.Equal(``, must(io.ReadAll(rs))) +} + +func must(b []byte, e error) string { + if e != nil { + panic(e) + } + + return string(b) +} diff --git a/store/rdbms/reports.gen.go b/store/rdbms/reports.gen.go index c229740b2..337de31fc 100644 --- a/store/rdbms/reports.gen.go +++ b/store/rdbms/reports.gen.go @@ -11,7 +11,6 @@ package rdbms import ( "context" "database/sql" - "github.com/Masterminds/squirrel" "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/pkg/filter" diff --git a/tests/apigw/main_test.go b/tests/apigw/main_test.go index ae55a8023..c8cefef23 100644 --- a/tests/apigw/main_test.go +++ b/tests/apigw/main_test.go @@ -51,7 +51,6 @@ var ( testApp *app.CortezaApp r chi.Router - // defStore store.Storer eventBus = eventbus.New() ) @@ -69,7 +68,6 @@ func InitTestApp() { return err } - // defStore = app.Store eventbus.Set(eventBus) return nil })