From 3ed83f0a57eb082380b0a24805b1fe80af562c20 Mon Sep 17 00:00:00 2001 From: Rafael Soares Date: Fri, 25 Nov 2022 19:28:30 -0300 Subject: [PATCH 01/18] flows external service and call --- assets/external_service.go | 63 ++++++++++++++++++++++++++ flows/actions/call_external_service.go | 30 ++++++++++++ flows/external_service.go | 42 +++++++++++++++++ flows/interfaces.go | 1 + 4 files changed, 136 insertions(+) create mode 100644 assets/external_service.go create mode 100644 flows/actions/call_external_service.go create mode 100644 flows/external_service.go diff --git a/assets/external_service.go b/assets/external_service.go new file mode 100644 index 000000000..aa24be723 --- /dev/null +++ b/assets/external_service.go @@ -0,0 +1,63 @@ +package assets + +import ( + "fmt" + + "github.com/nyaruka/gocommon/uuids" +) + +// ExternalServiceUUID is the UUID of a external service +type ExternalServiceUUID uuids.UUID + +// ExternalService is a third party service that can be called +// +// { +// "uuid": "4e19fc3c-ae17-4f6b-acb5-7d915e29dc27", +// "name": "Third party service integration 1", +// "uuid": "generic third party service", +// } +// +// @asset external_service +type ExternalService interface { + UUID() ExternalServiceUUID + Name() string + Type() string +} + +// ExternalServiceReference is used to reference a external service +type ExternalServiceReference struct { + UUID ExternalServiceUUID `json:"uuid" validate:"required,uuid"` + Name string `json:"name"` +} + +// NewExternalServiceReference creates a new external service reference with the given UUID and name +func NewExternalServiceReference(uuid ExternalServiceUUID, name string) *ExternalServiceReference { + return &ExternalServiceReference{UUID: uuid, Name: name} +} + +// Type returns the name of the asset type +func (r *ExternalServiceReference) Type() string { + return "external_service" +} + +// GenericUUID returns the untyped UUID +func (r *ExternalServiceReference) GenericUUID() uuids.UUID { + return uuids.UUID(r.UUID) +} + +// Identity retusns the unique identity of the asset +func (r *ExternalServiceReference) Identity() string { + return string(r.UUID) +} + +// Variable returns whether this a variable (vs concrete) reference +func (r *ExternalServiceReference) Variable() bool { + return false +} + +// String returns a formated string for the external service referenced +func (r *ExternalServiceReference) String() string { + return fmt.Sprintf("%s[uuid=%s,name=%s]", r.Type(), r.Identity(), r.Name) +} + +var _ UUIDReference = (*ExternalServiceReference)(nil) diff --git a/flows/actions/call_external_service.go b/flows/actions/call_external_service.go new file mode 100644 index 000000000..c2b6bcc5c --- /dev/null +++ b/flows/actions/call_external_service.go @@ -0,0 +1,30 @@ +package actions + +import ( + "github.com/nyaruka/goflow/assets" + "github.com/nyaruka/goflow/flows" +) + +func init() { + registerType(TypeCallExternalService, func() flows.Action { return &CallExternalServiceAction{} }) +} + +const TypeCallExternalService string = "call_external_service" + +type CallExternalServiceAction struct { + baseAction + onlineAction + ExternalService *assets.ExternalServiceReference + Input string + ResultName string +} + +func (a *CallExternalServiceAction) Execute(run flows.FlowRun, step flows.Step, logModifier flows.ModifierCallback, logEvent flows.EventCallback) error { + externalServices := run.Session().Assets().ExternalServices() + externalService := externalServices.Get(a.ExternalService.UUID) + return a.call(run, step, "", externalService, logEvent) +} + +func (a *CallExternalServiceAction) call(run flows.FlowRun, step flows.Step, input string, externalService *flows.ExternalService, logEvent flows.EventCallback) error { + return nil +} diff --git a/flows/external_service.go b/flows/external_service.go new file mode 100644 index 000000000..b4dec90e0 --- /dev/null +++ b/flows/external_service.go @@ -0,0 +1,42 @@ +package flows + +import "github.com/nyaruka/goflow/assets" + +// ExternalService represents an third party service integration. +type ExternalService struct { + assets.ExternalService +} + +// NewExternalService returns a new external service object from the given external service asset. +func NewExternalService(asset assets.ExternalService) *ExternalService { + return &ExternalService{ExternalService: asset} +} + +// Asset returns the underlying asset +func (e *ExternalService) Asset() assets.ExternalService { return e.ExternalService } + +// Reference returns a reference to this external service +func (e *ExternalService) Reference() *assets.ExternalServiceReference { + return assets.NewExternalServiceReference(e.UUID(), e.Name()) +} + +// ExternalServiceAssets provides access to all external services assets +type ExternalServiceAssets struct { + byUUID map[assets.ExternalServiceUUID]*ExternalService +} + +// NewExternalServiceAssets creates a new set of external service assets +func NewExternalServiceAssets(externalServices []assets.ExternalService) *ExternalServiceAssets { + s := &ExternalServiceAssets{ + byUUID: make(map[assets.ExternalServiceUUID]*ExternalService, len(externalServices)), + } + for _, asset := range externalServices { + s.byUUID[asset.UUID()] = NewExternalService(asset) + } + return s +} + +// Get returns the external service with the given UUID +func (s *ExternalServiceAssets) Get(uuid assets.ExternalServiceUUID) *ExternalService { + return s.byUUID[uuid] +} diff --git a/flows/interfaces.go b/flows/interfaces.go index 92bdb2937..026c733c9 100644 --- a/flows/interfaces.go +++ b/flows/interfaces.go @@ -102,6 +102,7 @@ type SessionAssets interface { Channels() *ChannelAssets Classifiers() *ClassifierAssets + ExternalServices() *ExternalServiceAssets Fields() *FieldAssets Flows() FlowAssets Globals() *GlobalAssets From 3ae89a341d6a8280c41da3836fd2dc8083bfcd90 Mon Sep 17 00:00:00 2001 From: Rafael Soares Date: Mon, 5 Dec 2022 15:47:21 -0300 Subject: [PATCH 02/18] externalservice source, action, etc... --- assets/source.go | 1 + assets/static/external_service.go | 30 +++++++ assets/static/source.go | 35 +++++--- flows/actions/call_external_service.go | 84 +++++++++++++++-- .../testdata/call_external_service.json | 1 + flows/engine/assets.go | 89 ++++++++++--------- flows/engine/assets_test.go | 4 + flows/engine/engine.go | 5 ++ flows/engine/services.go | 21 +++-- flows/events/service_called.go | 18 +++- flows/services.go | 12 +++ 11 files changed, 232 insertions(+), 68 deletions(-) create mode 100644 assets/static/external_service.go create mode 100644 flows/actions/testdata/call_external_service.json diff --git a/assets/source.go b/assets/source.go index c0955d5a2..27124a670 100644 --- a/assets/source.go +++ b/assets/source.go @@ -4,6 +4,7 @@ package assets type Source interface { Channels() ([]Channel, error) Classifiers() ([]Classifier, error) + ExternalServices() ([]ExternalService, error) Fields() ([]Field, error) Flow(FlowUUID) (Flow, error) Globals() ([]Global, error) diff --git a/assets/static/external_service.go b/assets/static/external_service.go new file mode 100644 index 000000000..d767ada5b --- /dev/null +++ b/assets/static/external_service.go @@ -0,0 +1,30 @@ +package static + +import ( + "github.com/nyaruka/goflow/assets" +) + +// ExternalService is a JSON serializable implementation of a external service asset +type ExternalService struct { + UUID_ assets.ExternalServiceUUID `json:"uuid" validate:"required,uuid` + Name_ string `json:"name"` + Type_ string `json:"type"` +} + +// NewExternalService creates a new external service +func NewExternalService(uuid assets.ExternalServiceUUID, name string, type_ string) assets.ExternalService { + return &ExternalService{ + UUID_: uuid, + Name_: name, + Type_: type_, + } +} + +// UUID returns the UUIUD of this external service +func (e *ExternalService) UUID() assets.ExternalServiceUUID { return e.UUID_ } + +// Name returns the name of this external service +func (e *ExternalService) Name() string { return e.Name_ } + +// Type returns the type of this external service +func (e *ExternalService) Type() string { return e.Type_ } diff --git a/assets/static/source.go b/assets/static/source.go index 5a884751d..e97c36faa 100644 --- a/assets/static/source.go +++ b/assets/static/source.go @@ -15,19 +15,20 @@ import ( // StaticSource is an asset source which loads assets from a static JSON file type StaticSource struct { s struct { - Channels []*Channel `json:"channels" validate:"omitempty,dive"` - Classifiers []*Classifier `json:"classifiers" validate:"omitempty,dive"` - Fields []*Field `json:"fields" validate:"omitempty,dive"` - Flows []*Flow `json:"flows" validate:"omitempty,dive"` - Globals []*Global `json:"globals" validate:"omitempty,dive"` - Groups []*Group `json:"groups" validate:"omitempty,dive"` - Labels []*Label `json:"labels" validate:"omitempty,dive"` - Locations []*envs.LocationHierarchy `json:"locations"` - Resthooks []*Resthook `json:"resthooks" validate:"omitempty,dive"` - Templates []*Template `json:"templates" validate:"omitempty,dive"` - Ticketers []*Ticketer `json:"ticketers" validate:"omitempty,dive"` - Topics []*Topic `json:"topics" validate:"omitempty,dive"` - Users []*User `json:"users" validate:"omitempty,dive"` + Channels []*Channel `json:"channels" validate:"omitempty,dive"` + Classifiers []*Classifier `json:"classifiers" validate:"omitempty,dive"` + ExternalServices []*ExternalService `json:"externalServices" validate:"omitempty"` + Fields []*Field `json:"fields" validate:"omitempty,dive"` + Flows []*Flow `json:"flows" validate:"omitempty,dive"` + Globals []*Global `json:"globals" validate:"omitempty,dive"` + Groups []*Group `json:"groups" validate:"omitempty,dive"` + Labels []*Label `json:"labels" validate:"omitempty,dive"` + Locations []*envs.LocationHierarchy `json:"locations"` + Resthooks []*Resthook `json:"resthooks" validate:"omitempty,dive"` + Templates []*Template `json:"templates" validate:"omitempty,dive"` + Ticketers []*Ticketer `json:"ticketers" validate:"omitempty,dive"` + Topics []*Topic `json:"topics" validate:"omitempty,dive"` + Users []*User `json:"users" validate:"omitempty,dive"` } } @@ -74,6 +75,14 @@ func (s *StaticSource) Classifiers() ([]assets.Classifier, error) { return set, nil } +func (s *StaticSource) ExternalServices() ([]assets.ExternalService, error) { + set := make([]assets.ExternalService, len(s.s.ExternalServices)) + for i := range s.s.ExternalServices { + set[i] = s.s.ExternalServices[i] + } + return set, nil +} + // Fields returns all field assets func (s *StaticSource) Fields() ([]assets.Field, error) { set := make([]assets.Field, len(s.s.Fields)) diff --git a/flows/actions/call_external_service.go b/flows/actions/call_external_service.go index c2b6bcc5c..5bdd0da6b 100644 --- a/flows/actions/call_external_service.go +++ b/flows/actions/call_external_service.go @@ -1,30 +1,104 @@ package actions import ( + "fmt" + "github.com/nyaruka/goflow/assets" "github.com/nyaruka/goflow/flows" + "github.com/nyaruka/goflow/flows/events" ) func init() { registerType(TypeCallExternalService, func() flows.Action { return &CallExternalServiceAction{} }) } +var externalServiceCategories = []string{CategorySuccess, CategoryFailure} + +// TypeCallExternalService is the type for the external service calls action const TypeCallExternalService string = "call_external_service" +// CallExternalServiceAction is used to call a external service in the context of contact +// { +// "uuid": "3ed82e8e-409c-46b8-99ec-4ef9c7ec0270", +// "type": "call_external_service", +// "method": "GET", +// "external_service": { +// "uuid": "0ebd32fd-362b-4253-89a1-3796aa499b82", +// "name": "service foo", +// }, +// "header: "{"key1":"value1"}", +// "query": "{"query":"value"}", +// "body": "{"key1":"value1", "key2":"value2"}" +// "result_name": "external_service_call" +// } type CallExternalServiceAction struct { baseAction onlineAction - ExternalService *assets.ExternalServiceReference - Input string - ResultName string + + ExternalService *assets.ExternalServiceReference `json:"external_service,omitempty"` + Header map[string]string `json:"header,omitempty"` + Query map[string]string `json:"query,omitempty"` + Body string `json:"input,omitempty"` + ResultName string `json:"result_name,omitempty"` +} + +func NewCallExternalService(uuid flows.ActionUUID, externalService *assets.ExternalServiceReference, header map[string]string, query map[string]string, body string, resultName string) *CallExternalServiceAction { + return &CallExternalServiceAction{ + baseAction: newBaseAction(TypeCallExternalService, uuid), + ExternalService: externalService, + Header: header, + Query: query, + Body: body, + ResultName: resultName, + } } func (a *CallExternalServiceAction) Execute(run flows.FlowRun, step flows.Step, logModifier flows.ModifierCallback, logEvent flows.EventCallback) error { externalServices := run.Session().Assets().ExternalServices() externalService := externalServices.Get(a.ExternalService.UUID) - return a.call(run, step, "", externalService, logEvent) + + evaluetedBody, err := run.EvaluateTemplate(a.Body) + if err != nil { + logEvent(events.NewError(err)) + } + + return a.call(run, step, externalService, evaluetedBody, logEvent) } -func (a *CallExternalServiceAction) call(run flows.FlowRun, step flows.Step, input string, externalService *flows.ExternalService, logEvent flows.EventCallback) error { +func (a *CallExternalServiceAction) call(run flows.FlowRun, step flows.Step, externalService *flows.ExternalService, body string, logEvent flows.EventCallback) error { + if externalService == nil { + logEvent(events.NewDependencyError(a.ExternalService)) + return nil + } + + svc, err := run.Session().Engine().Services().ExternalService(run.Session(), externalService) + if err != nil { + logEvent(events.NewError(err)) + return nil + } + + httpLogger := &flows.HTTPLogger{} + + call, err := svc.Call(run.Session(), body, httpLogger.Log) + if err != nil { + logEvent(events.NewError(err)) + } + if len(httpLogger.Logs) > 0 { + logEvent(events.NewExternalServiceCalled(externalService.Reference(), httpLogger.Logs)) + } + + if call != nil { + if a.ResultName != "" { + input := fmt.Sprintf("%s %s", call.Request.Method, call.Request.URL.String()) + a.saveResult(run, step, a.ResultName, string(call.ResponseJSON), CategorySuccess, "", input, call.ResponseJSON, logEvent) + } + } + return nil } + +func (a *CallExternalServiceAction) Results(include func(*flows.ResultInfo)) { + if a.ResultName != "" { + include(flows.NewResultInfo(a.ResultName, externalServiceCategories)) + } +} diff --git a/flows/actions/testdata/call_external_service.json b/flows/actions/testdata/call_external_service.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/flows/actions/testdata/call_external_service.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/flows/engine/assets.go b/flows/engine/assets.go index 1be6fc39c..acd78991a 100644 --- a/flows/engine/assets.go +++ b/flows/engine/assets.go @@ -12,19 +12,20 @@ import ( type sessionAssets struct { source assets.Source - channels *flows.ChannelAssets - classifiers *flows.ClassifierAssets - fields *flows.FieldAssets - flows flows.FlowAssets - globals *flows.GlobalAssets - groups *flows.GroupAssets - labels *flows.LabelAssets - locations *flows.LocationAssets - resthooks *flows.ResthookAssets - templates *flows.TemplateAssets - ticketers *flows.TicketerAssets - topics *flows.TopicAssets - users *flows.UserAssets + channels *flows.ChannelAssets + classifiers *flows.ClassifierAssets + externalServices *flows.ExternalServiceAssets + fields *flows.FieldAssets + flows flows.FlowAssets + globals *flows.GlobalAssets + groups *flows.GroupAssets + labels *flows.LabelAssets + locations *flows.LocationAssets + resthooks *flows.ResthookAssets + templates *flows.TemplateAssets + ticketers *flows.TicketerAssets + topics *flows.TopicAssets + users *flows.UserAssets } var _ flows.SessionAssets = (*sessionAssets)(nil) @@ -39,6 +40,10 @@ func NewSessionAssets(env envs.Environment, source assets.Source, migrationConfi if err != nil { return nil, err } + externalServices, err := source.ExternalServices() + if err != nil { + return nil, err + } fields, err := source.Fields() if err != nil { return nil, err @@ -84,37 +89,39 @@ func NewSessionAssets(env envs.Environment, source assets.Source, migrationConfi groupAssets, _ := flows.NewGroupAssets(env, fieldAssets, groups) return &sessionAssets{ - source: source, - channels: flows.NewChannelAssets(channels), - classifiers: flows.NewClassifierAssets(classifiers), - fields: fieldAssets, - flows: definition.NewFlowAssets(source, migrationConfig), - globals: flows.NewGlobalAssets(globals), - groups: groupAssets, - labels: flows.NewLabelAssets(labels), - locations: flows.NewLocationAssets(locations), - resthooks: flows.NewResthookAssets(resthooks), - templates: flows.NewTemplateAssets(templates), - ticketers: flows.NewTicketerAssets(ticketers), - topics: flows.NewTopicAssets(topics), - users: flows.NewUserAssets(users), + source: source, + channels: flows.NewChannelAssets(channels), + classifiers: flows.NewClassifierAssets(classifiers), + externalServices: flows.NewExternalServiceAssets(externalServices), + fields: fieldAssets, + flows: definition.NewFlowAssets(source, migrationConfig), + globals: flows.NewGlobalAssets(globals), + groups: groupAssets, + labels: flows.NewLabelAssets(labels), + locations: flows.NewLocationAssets(locations), + resthooks: flows.NewResthookAssets(resthooks), + templates: flows.NewTemplateAssets(templates), + ticketers: flows.NewTicketerAssets(ticketers), + topics: flows.NewTopicAssets(topics), + users: flows.NewUserAssets(users), }, nil } -func (s *sessionAssets) Source() assets.Source { return s.source } -func (s *sessionAssets) Channels() *flows.ChannelAssets { return s.channels } -func (s *sessionAssets) Classifiers() *flows.ClassifierAssets { return s.classifiers } -func (s *sessionAssets) Fields() *flows.FieldAssets { return s.fields } -func (s *sessionAssets) Flows() flows.FlowAssets { return s.flows } -func (s *sessionAssets) Globals() *flows.GlobalAssets { return s.globals } -func (s *sessionAssets) Groups() *flows.GroupAssets { return s.groups } -func (s *sessionAssets) Labels() *flows.LabelAssets { return s.labels } -func (s *sessionAssets) Locations() *flows.LocationAssets { return s.locations } -func (s *sessionAssets) Resthooks() *flows.ResthookAssets { return s.resthooks } -func (s *sessionAssets) Templates() *flows.TemplateAssets { return s.templates } -func (s *sessionAssets) Ticketers() *flows.TicketerAssets { return s.ticketers } -func (s *sessionAssets) Topics() *flows.TopicAssets { return s.topics } -func (s *sessionAssets) Users() *flows.UserAssets { return s.users } +func (s *sessionAssets) Source() assets.Source { return s.source } +func (s *sessionAssets) Channels() *flows.ChannelAssets { return s.channels } +func (s *sessionAssets) Classifiers() *flows.ClassifierAssets { return s.classifiers } +func (s *sessionAssets) ExternalServices() *flows.ExternalServiceAssets { return s.externalServices } +func (s *sessionAssets) Fields() *flows.FieldAssets { return s.fields } +func (s *sessionAssets) Flows() flows.FlowAssets { return s.flows } +func (s *sessionAssets) Globals() *flows.GlobalAssets { return s.globals } +func (s *sessionAssets) Groups() *flows.GroupAssets { return s.groups } +func (s *sessionAssets) Labels() *flows.LabelAssets { return s.labels } +func (s *sessionAssets) Locations() *flows.LocationAssets { return s.locations } +func (s *sessionAssets) Resthooks() *flows.ResthookAssets { return s.resthooks } +func (s *sessionAssets) Templates() *flows.TemplateAssets { return s.templates } +func (s *sessionAssets) Ticketers() *flows.TicketerAssets { return s.ticketers } +func (s *sessionAssets) Topics() *flows.TopicAssets { return s.topics } +func (s *sessionAssets) Users() *flows.UserAssets { return s.users } func (s *sessionAssets) ResolveField(key string) assets.Field { f := s.Fields().Get(key) diff --git a/flows/engine/assets_test.go b/flows/engine/assets_test.go index 7438d7a64..2230f8744 100644 --- a/flows/engine/assets_test.go +++ b/flows/engine/assets_test.go @@ -106,6 +106,10 @@ func (s *testSource) Classifiers() ([]assets.Classifier, error) { return nil, s.err("classifiers") } +func (s *testSource) ExternalServices() ([]assets.ExternalService, error) { + return nil, s.err("externalServices") +} + func (s *testSource) Fields() ([]assets.Field, error) { return nil, s.err("fields") } diff --git a/flows/engine/engine.go b/flows/engine/engine.go index 002d2bbdd..93273b08a 100644 --- a/flows/engine/engine.go +++ b/flows/engine/engine.go @@ -96,6 +96,11 @@ func (b *Builder) WithAirtimeServiceFactory(f AirtimeServiceFactory) *Builder { return b } +func (b *Builder) WithExternalServiceServiceFactory(f ExternalServiceServiceFactory) *Builder { + b.eng.services.externalService = f + return b +} + // WithMaxStepsPerSprint sets the maximum number of steps allowed in a single sprint func (b *Builder) WithMaxStepsPerSprint(max int) *Builder { b.eng.maxStepsPerSprint = max diff --git a/flows/engine/services.go b/flows/engine/services.go index fc57e88be..26ea9606d 100644 --- a/flows/engine/services.go +++ b/flows/engine/services.go @@ -21,12 +21,16 @@ type TicketServiceFactory func(flows.Session, *flows.Ticketer) (flows.TicketServ // AirtimeServiceFactory resolves a session to an airtime service type AirtimeServiceFactory func(flows.Session) (flows.AirtimeService, error) +// ExternalServiceServiceFactory resolves a session to an external service service +type ExternalServiceServiceFactory func(flows.Session, *flows.ExternalService) (flows.ExternalServiceService, error) + type services struct { - email EmailServiceFactory - webhook WebhookServiceFactory - classification ClassificationServiceFactory - ticket TicketServiceFactory - airtime AirtimeServiceFactory + email EmailServiceFactory + webhook WebhookServiceFactory + classification ClassificationServiceFactory + ticket TicketServiceFactory + airtime AirtimeServiceFactory + externalService ExternalServiceServiceFactory } func newEmptyServices() *services { @@ -46,6 +50,9 @@ func newEmptyServices() *services { airtime: func(flows.Session) (flows.AirtimeService, error) { return nil, errors.New("no airtime service factory configured") }, + externalService: func(flows.Session, *flows.ExternalService) (flows.ExternalServiceService, error) { + return nil, errors.New("no external service factory configured") + }, } } @@ -68,3 +75,7 @@ func (s *services) Ticket(session flows.Session, ticketer *flows.Ticketer) (flow func (s *services) Airtime(session flows.Session) (flows.AirtimeService, error) { return s.airtime(session) } + +func (s *services) ExternalService(session flows.Session, externalService *flows.ExternalService) (flows.ExternalServiceService, error) { + return s.externalService(session, externalService) +} diff --git a/flows/events/service_called.go b/flows/events/service_called.go index ea249e76e..6741f2b1c 100644 --- a/flows/events/service_called.go +++ b/flows/events/service_called.go @@ -35,10 +35,11 @@ const TypeServiceCalled string = "service_called" type ServiceCalledEvent struct { baseEvent - Service string `json:"service"` - Classifier *assets.ClassifierReference `json:"classifier,omitempty"` - Ticketer *assets.TicketerReference `json:"ticketer,omitempty"` - HTTPLogs []*flows.HTTPLog `json:"http_logs"` + Service string `json:"service"` + Classifier *assets.ClassifierReference `json:"classifier,omitempty"` + Ticketer *assets.TicketerReference `json:"ticketer,omitempty"` + ExternalService *assets.ExternalServiceReference `json:"external_service,omitempty"` + HTTPLogs []*flows.HTTPLog `json:"http_logs"` } // NewClassifierCalled returns a service called event for a classifier @@ -60,3 +61,12 @@ func NewTicketerCalled(ticketer *assets.TicketerReference, httpLogs []*flows.HTT HTTPLogs: httpLogs, } } + +func NewExternalServiceCalled(externalService *assets.ExternalServiceReference, httpLogs []*flows.HTTPLog) *ServiceCalledEvent { + return &ServiceCalledEvent{ + baseEvent: newBaseEvent(TypeServiceCalled), + Service: "external_service", + ExternalService: externalService, + HTTPLogs: httpLogs, + } +} diff --git a/flows/services.go b/flows/services.go index d4c01100b..d5ad618fb 100644 --- a/flows/services.go +++ b/flows/services.go @@ -19,6 +19,7 @@ type Services interface { Classification(Session, *Classifier) (ClassificationService, error) Ticket(Session, *Ticketer) (TicketService, error) Airtime(Session) (AirtimeService, error) + ExternalService(Session, *ExternalService) (ExternalServiceService, error) } // EmailService provides email functionality to the engine @@ -73,6 +74,13 @@ type Classification struct { Entities map[string][]ExtractedEntity `json:"entities,omitempty"` } +// ExternalServiceCall is the result of a external service call +type ExternalServiceCall struct { + *httpx.Trace + ResponseJSON []byte + ResponseCleaned bool +} + // ClassificationService provides NLU functionality to the engine type ClassificationService interface { Classify(session Session, input string, logHTTP HTTPLogCallback) (*Classification, error) @@ -84,6 +92,10 @@ type TicketService interface { Open(session Session, topic *Topic, body string, assignee *User, logHTTP HTTPLogCallback) (*Ticket, error) } +type ExternalServiceService interface { + Call(sesion Session, body string, logHTTP HTTPLogCallback) (*ExternalServiceCall, error) +} + // AirtimeTransferStatus is a status of a airtime transfer type AirtimeTransferStatus string From aba1234d6a29c6f83f3818118e3e8d749a3937fd Mon Sep 17 00:00:00 2001 From: Rafael Soares Date: Tue, 28 Feb 2023 15:45:03 -0300 Subject: [PATCH 03/18] refactor external service integration to receive params --- assets/external_service.go | 40 +++++++++++++++++ flows/actions/base_test.go | 61 ++++++++++++++++++++++++++ flows/actions/call_external_service.go | 26 +++-------- flows/services.go | 3 +- 4 files changed, 110 insertions(+), 20 deletions(-) diff --git a/assets/external_service.go b/assets/external_service.go index aa24be723..e9d9018b1 100644 --- a/assets/external_service.go +++ b/assets/external_service.go @@ -30,6 +30,46 @@ type ExternalServiceReference struct { Name string `json:"name"` } +type ExternalServiceParam struct { + Data struct { + Value string `json:"value,omitempty"` + } `json:"data,omitempty"` + Filter struct { + Value *ExternalServiceFilterValue `json:"value"` + } `json:"filter,omitempty"` + Type string `json:"type,omitempty"` + VerboseName string `json:"verboseName,omitempty"` +} + +type ExternalServiceFilterValue struct { + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + VerboseName string `json:"verboseName,omitempty"` +} + +func NewExternalServiceParam( + dataValue, + filterName, + filterType, + filterVerboseName, + pType, + verboseName string, +) *ExternalServiceParam { + p := &ExternalServiceParam{} + p.Data.Value = dataValue + if filterName != "" && filterType != "" && filterVerboseName != "" { + p.Filter.Value = &ExternalServiceFilterValue{} + p.Filter.Value.Name = filterName + p.Filter.Value.Type = filterType + p.Filter.Value.VerboseName = filterVerboseName + } else { + p.Filter.Value = nil + } + p.Type = pType + p.VerboseName = verboseName + return p +} + // NewExternalServiceReference creates a new external service reference with the given UUID and name func NewExternalServiceReference(uuid ExternalServiceUUID, name string) *ExternalServiceReference { return &ExternalServiceReference{UUID: uuid, Name: name} diff --git a/flows/actions/base_test.go b/flows/actions/base_test.go index a0277c009..e80c366e4 100644 --- a/flows/actions/base_test.go +++ b/flows/actions/base_test.go @@ -720,6 +720,67 @@ func TestConstructors(t *testing.T) { "create_contact": true }`, }, + { + actions.NewCallExternalService( + actionUUID, + assets.NewExternalServiceReference(assets.ExternalServiceUUID("75a246f4-6fe8-43bd-8e7f-3da1f69b4507"), "external service"), + []assets.ExternalServiceParam{ + *assets.NewExternalServiceParam("123", "nCod", "integer", "Código do Contato", "identificacao", "Identificação"), + *assets.NewExternalServiceParam("fulano", "cNome", "string", "Nome do Contato", "identificacao", "Identificação"), + *assets.NewExternalServiceParam("obs1", "", "", "", "cObs", "Observações"), + }, + "ExternalServiceResponse", + ), + `{ + "external_service": { + "name": "external service", + "uuid": "75a246f4-6fe8-43bd-8e7f-3da1f69b4507" + }, + "type": "call_external_service", + "params": [ + { + "data": { + "value": "123" + }, + "filter": { + "value": { + "name": "nCod", + "type": "integer", + "verboseName": "Código do Contato" + } + }, + "type": "identificacao", + "verboseName": "Identificação" + }, + { + "data": { + "value": "fulano" + }, + "filter": { + "value": { + "name": "cNome", + "type": "string", + "verboseName": "Nome do Contato" + } + }, + "type": "identificacao", + "verboseName": "Identificação" + }, + { + "data": { + "value": "obs1" + }, + "filter": { + "value": null + }, + "type": "cObs", + "verboseName": "Observações" + } + ], + "result_name": "ExternalServiceResponse", + "uuid": "ad154980-7bf7-4ab8-8728-545fd6378912" + }`, + }, } for _, tc := range tests { diff --git a/flows/actions/call_external_service.go b/flows/actions/call_external_service.go index 5bdd0da6b..17b243ae1 100644 --- a/flows/actions/call_external_service.go +++ b/flows/actions/call_external_service.go @@ -21,14 +21,11 @@ const TypeCallExternalService string = "call_external_service" // { // "uuid": "3ed82e8e-409c-46b8-99ec-4ef9c7ec0270", // "type": "call_external_service", -// "method": "GET", // "external_service": { // "uuid": "0ebd32fd-362b-4253-89a1-3796aa499b82", // "name": "service foo", // }, -// "header: "{"key1":"value1"}", -// "query": "{"query":"value"}", -// "body": "{"key1":"value1", "key2":"value2"}" +// "params": [{"data":{"value":"foo"}, "filter": {"value":{"name":"foo","type":"bar","verboseName":"barz"}}, "type": "foo", "verboseName": "bar"}], // "result_name": "external_service_call" // } type CallExternalServiceAction struct { @@ -36,19 +33,15 @@ type CallExternalServiceAction struct { onlineAction ExternalService *assets.ExternalServiceReference `json:"external_service,omitempty"` - Header map[string]string `json:"header,omitempty"` - Query map[string]string `json:"query,omitempty"` - Body string `json:"input,omitempty"` + Params []assets.ExternalServiceParam `json:"params,omitempty"` ResultName string `json:"result_name,omitempty"` } -func NewCallExternalService(uuid flows.ActionUUID, externalService *assets.ExternalServiceReference, header map[string]string, query map[string]string, body string, resultName string) *CallExternalServiceAction { +func NewCallExternalService(uuid flows.ActionUUID, externalService *assets.ExternalServiceReference, params []assets.ExternalServiceParam, resultName string) *CallExternalServiceAction { return &CallExternalServiceAction{ baseAction: newBaseAction(TypeCallExternalService, uuid), ExternalService: externalService, - Header: header, - Query: query, - Body: body, + Params: params, ResultName: resultName, } } @@ -57,15 +50,10 @@ func (a *CallExternalServiceAction) Execute(run flows.FlowRun, step flows.Step, externalServices := run.Session().Assets().ExternalServices() externalService := externalServices.Get(a.ExternalService.UUID) - evaluetedBody, err := run.EvaluateTemplate(a.Body) - if err != nil { - logEvent(events.NewError(err)) - } - - return a.call(run, step, externalService, evaluetedBody, logEvent) + return a.call(run, step, externalService, a.Params, logEvent) } -func (a *CallExternalServiceAction) call(run flows.FlowRun, step flows.Step, externalService *flows.ExternalService, body string, logEvent flows.EventCallback) error { +func (a *CallExternalServiceAction) call(run flows.FlowRun, step flows.Step, externalService *flows.ExternalService, params []assets.ExternalServiceParam, logEvent flows.EventCallback) error { if externalService == nil { logEvent(events.NewDependencyError(a.ExternalService)) return nil @@ -79,7 +67,7 @@ func (a *CallExternalServiceAction) call(run flows.FlowRun, step flows.Step, ext httpLogger := &flows.HTTPLogger{} - call, err := svc.Call(run.Session(), body, httpLogger.Log) + call, err := svc.Call(run.Session(), params, httpLogger.Log) if err != nil { logEvent(events.NewError(err)) } diff --git a/flows/services.go b/flows/services.go index d5ad618fb..46cc8249e 100644 --- a/flows/services.go +++ b/flows/services.go @@ -7,6 +7,7 @@ import ( "github.com/nyaruka/gocommon/httpx" "github.com/nyaruka/gocommon/urns" "github.com/nyaruka/gocommon/uuids" + "github.com/nyaruka/goflow/assets" "github.com/nyaruka/goflow/utils" "github.com/shopspring/decimal" @@ -93,7 +94,7 @@ type TicketService interface { } type ExternalServiceService interface { - Call(sesion Session, body string, logHTTP HTTPLogCallback) (*ExternalServiceCall, error) + Call(sesion Session, params []assets.ExternalServiceParam, logHTTP HTTPLogCallback) (*ExternalServiceCall, error) } // AirtimeTransferStatus is a status of a airtime transfer From fdd024914d3cee00057341c80a81aa25e50098cb Mon Sep 17 00:00:00 2001 From: Rafael Soares Date: Tue, 28 Feb 2023 16:31:04 -0300 Subject: [PATCH 04/18] add asset struct externalservicecallaction --- assets/external_service.go | 5 +++++ flows/actions/base_test.go | 1 + flows/actions/call_external_service.go | 10 ++++++---- flows/services.go | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/assets/external_service.go b/assets/external_service.go index e9d9018b1..fd4b50ace 100644 --- a/assets/external_service.go +++ b/assets/external_service.go @@ -30,6 +30,11 @@ type ExternalServiceReference struct { Name string `json:"name"` } +type ExternalServiceCallAction struct { + Name string `json:"name"` + Value string `json:"value"` +} + type ExternalServiceParam struct { Data struct { Value string `json:"value,omitempty"` diff --git a/flows/actions/base_test.go b/flows/actions/base_test.go index e80c366e4..cbdea8317 100644 --- a/flows/actions/base_test.go +++ b/flows/actions/base_test.go @@ -724,6 +724,7 @@ func TestConstructors(t *testing.T) { actions.NewCallExternalService( actionUUID, assets.NewExternalServiceReference(assets.ExternalServiceUUID("75a246f4-6fe8-43bd-8e7f-3da1f69b4507"), "external service"), + assets.ExternalServiceCallAction{Name: "IncluirContato"}, []assets.ExternalServiceParam{ *assets.NewExternalServiceParam("123", "nCod", "integer", "Código do Contato", "identificacao", "Identificação"), *assets.NewExternalServiceParam("fulano", "cNome", "string", "Nome do Contato", "identificacao", "Identificação"), diff --git a/flows/actions/call_external_service.go b/flows/actions/call_external_service.go index 17b243ae1..bcd1ee7fb 100644 --- a/flows/actions/call_external_service.go +++ b/flows/actions/call_external_service.go @@ -33,14 +33,16 @@ type CallExternalServiceAction struct { onlineAction ExternalService *assets.ExternalServiceReference `json:"external_service,omitempty"` + CallAction assets.ExternalServiceCallAction `json:"call"` Params []assets.ExternalServiceParam `json:"params,omitempty"` ResultName string `json:"result_name,omitempty"` } -func NewCallExternalService(uuid flows.ActionUUID, externalService *assets.ExternalServiceReference, params []assets.ExternalServiceParam, resultName string) *CallExternalServiceAction { +func NewCallExternalService(uuid flows.ActionUUID, externalService *assets.ExternalServiceReference, callAction assets.ExternalServiceCallAction, params []assets.ExternalServiceParam, resultName string) *CallExternalServiceAction { return &CallExternalServiceAction{ baseAction: newBaseAction(TypeCallExternalService, uuid), ExternalService: externalService, + CallAction: callAction, Params: params, ResultName: resultName, } @@ -50,10 +52,10 @@ func (a *CallExternalServiceAction) Execute(run flows.FlowRun, step flows.Step, externalServices := run.Session().Assets().ExternalServices() externalService := externalServices.Get(a.ExternalService.UUID) - return a.call(run, step, externalService, a.Params, logEvent) + return a.call(run, step, externalService, a.CallAction, a.Params, logEvent) } -func (a *CallExternalServiceAction) call(run flows.FlowRun, step flows.Step, externalService *flows.ExternalService, params []assets.ExternalServiceParam, logEvent flows.EventCallback) error { +func (a *CallExternalServiceAction) call(run flows.FlowRun, step flows.Step, externalService *flows.ExternalService, callAction assets.ExternalServiceCallAction, params []assets.ExternalServiceParam, logEvent flows.EventCallback) error { if externalService == nil { logEvent(events.NewDependencyError(a.ExternalService)) return nil @@ -67,7 +69,7 @@ func (a *CallExternalServiceAction) call(run flows.FlowRun, step flows.Step, ext httpLogger := &flows.HTTPLogger{} - call, err := svc.Call(run.Session(), params, httpLogger.Log) + call, err := svc.Call(run.Session(), callAction, params, httpLogger.Log) if err != nil { logEvent(events.NewError(err)) } diff --git a/flows/services.go b/flows/services.go index 46cc8249e..5e246e7ce 100644 --- a/flows/services.go +++ b/flows/services.go @@ -94,7 +94,7 @@ type TicketService interface { } type ExternalServiceService interface { - Call(sesion Session, params []assets.ExternalServiceParam, logHTTP HTTPLogCallback) (*ExternalServiceCall, error) + Call(sesion Session, callAction assets.ExternalServiceCallAction, params []assets.ExternalServiceParam, logHTTP HTTPLogCallback) (*ExternalServiceCall, error) } // AirtimeTransferStatus is a status of a airtime transfer From 649dc0f6cee9f5648e723658cde5a09d6a8ee6b9 Mon Sep 17 00:00:00 2001 From: Rafael Soares Date: Fri, 3 Mar 2023 16:42:25 -0300 Subject: [PATCH 05/18] add ExternalServiceReference case on CheckReference --- flows/inspect/dependencies.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flows/inspect/dependencies.go b/flows/inspect/dependencies.go index 1c2ed2e63..7fb8f47c5 100644 --- a/flows/inspect/dependencies.go +++ b/flows/inspect/dependencies.go @@ -91,6 +91,8 @@ func CheckReference(sa flows.SessionAssets, ref assets.Reference) bool { return sa.Topics().Get(typed.UUID) != nil case *assets.UserReference: return sa.Users().Get(typed.Email) != nil + case *assets.ExternalServiceReference: + return sa.ExternalServices().Get(typed.UUID) != nil default: panic(fmt.Sprintf("unknown dependency type reference: %T", ref)) } From 4a9c1ea2e2c41eb7e2090ff496b37f973c26b785 Mon Sep 17 00:00:00 2001 From: Rafael Soares Date: Fri, 3 Mar 2023 19:21:43 -0300 Subject: [PATCH 06/18] tweak call_external_service call saveresult step --- flows/actions/call_external_service.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/flows/actions/call_external_service.go b/flows/actions/call_external_service.go index bcd1ee7fb..56694d464 100644 --- a/flows/actions/call_external_service.go +++ b/flows/actions/call_external_service.go @@ -1,8 +1,6 @@ package actions import ( - "fmt" - "github.com/nyaruka/goflow/assets" "github.com/nyaruka/goflow/flows" "github.com/nyaruka/goflow/flows/events" @@ -79,8 +77,7 @@ func (a *CallExternalServiceAction) call(run flows.FlowRun, step flows.Step, ext if call != nil { if a.ResultName != "" { - input := fmt.Sprintf("%s %s", call.Request.Method, call.Request.URL.String()) - a.saveResult(run, step, a.ResultName, string(call.ResponseJSON), CategorySuccess, "", input, call.ResponseJSON, logEvent) + a.saveResult(run, step, a.ResultName, string(call.ResponseJSON), CategorySuccess, "", "", call.ResponseJSON, logEvent) } } From 51614454fd4ed698b36535a53a82e65ae19a6021 Mon Sep 17 00:00:00 2001 From: Rafael Soares Date: Tue, 7 Mar 2023 11:10:54 -0300 Subject: [PATCH 07/18] externalservicecall struct tweaks --- flows/actions/base_test.go | 6 +++++- flows/actions/call_external_service.go | 6 +++++- flows/services.go | 3 ++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/flows/actions/base_test.go b/flows/actions/base_test.go index cbdea8317..84bd32cef 100644 --- a/flows/actions/base_test.go +++ b/flows/actions/base_test.go @@ -724,7 +724,7 @@ func TestConstructors(t *testing.T) { actions.NewCallExternalService( actionUUID, assets.NewExternalServiceReference(assets.ExternalServiceUUID("75a246f4-6fe8-43bd-8e7f-3da1f69b4507"), "external service"), - assets.ExternalServiceCallAction{Name: "IncluirContato"}, + assets.ExternalServiceCallAction{Name: "IncluirContato", Value: "IncluirContato"}, []assets.ExternalServiceParam{ *assets.NewExternalServiceParam("123", "nCod", "integer", "Código do Contato", "identificacao", "Identificação"), *assets.NewExternalServiceParam("fulano", "cNome", "string", "Nome do Contato", "identificacao", "Identificação"), @@ -738,6 +738,10 @@ func TestConstructors(t *testing.T) { "uuid": "75a246f4-6fe8-43bd-8e7f-3da1f69b4507" }, "type": "call_external_service", + "call": { + "name": "IncluirContato", + "value": "IncluirContato" + }, "params": [ { "data": { diff --git a/flows/actions/call_external_service.go b/flows/actions/call_external_service.go index 56694d464..9fae1db68 100644 --- a/flows/actions/call_external_service.go +++ b/flows/actions/call_external_service.go @@ -1,6 +1,8 @@ package actions import ( + "fmt" + "github.com/nyaruka/goflow/assets" "github.com/nyaruka/goflow/flows" "github.com/nyaruka/goflow/flows/events" @@ -23,6 +25,7 @@ const TypeCallExternalService string = "call_external_service" // "uuid": "0ebd32fd-362b-4253-89a1-3796aa499b82", // "name": "service foo", // }, +// "call": {"name": "foo", "value": "bar"}, // "params": [{"data":{"value":"foo"}, "filter": {"value":{"name":"foo","type":"bar","verboseName":"barz"}}, "type": "foo", "verboseName": "bar"}], // "result_name": "external_service_call" // } @@ -77,7 +80,8 @@ func (a *CallExternalServiceAction) call(run flows.FlowRun, step flows.Step, ext if call != nil { if a.ResultName != "" { - a.saveResult(run, step, a.ResultName, string(call.ResponseJSON), CategorySuccess, "", "", call.ResponseJSON, logEvent) + input := fmt.Sprintf("%s %s", call.RequestMethod, call.RequestURL) + a.saveResult(run, step, a.ResultName, string(call.ResponseJSON), CategorySuccess, "", input, call.ResponseJSON, logEvent) } } diff --git a/flows/services.go b/flows/services.go index 5e246e7ce..624e3d3e7 100644 --- a/flows/services.go +++ b/flows/services.go @@ -77,7 +77,8 @@ type Classification struct { // ExternalServiceCall is the result of a external service call type ExternalServiceCall struct { - *httpx.Trace + RequestMethod string + RequestURL string ResponseJSON []byte ResponseCleaned bool } From e30c8889251412a189f569ee04b29e2ac14cf8d0 Mon Sep 17 00:00:00 2001 From: Robi9 Date: Tue, 4 Apr 2023 12:29:26 -0300 Subject: [PATCH 08/18] Add params variable substitution --- flows/actions/call_external_service.go | 32 +++++++++++++++++--------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/flows/actions/call_external_service.go b/flows/actions/call_external_service.go index 9fae1db68..184482155 100644 --- a/flows/actions/call_external_service.go +++ b/flows/actions/call_external_service.go @@ -18,17 +18,18 @@ var externalServiceCategories = []string{CategorySuccess, CategoryFailure} const TypeCallExternalService string = "call_external_service" // CallExternalServiceAction is used to call a external service in the context of contact -// { -// "uuid": "3ed82e8e-409c-46b8-99ec-4ef9c7ec0270", -// "type": "call_external_service", -// "external_service": { -// "uuid": "0ebd32fd-362b-4253-89a1-3796aa499b82", -// "name": "service foo", -// }, -// "call": {"name": "foo", "value": "bar"}, -// "params": [{"data":{"value":"foo"}, "filter": {"value":{"name":"foo","type":"bar","verboseName":"barz"}}, "type": "foo", "verboseName": "bar"}], -// "result_name": "external_service_call" -// } +// +// { +// "uuid": "3ed82e8e-409c-46b8-99ec-4ef9c7ec0270", +// "type": "call_external_service", +// "external_service": { +// "uuid": "0ebd32fd-362b-4253-89a1-3796aa499b82", +// "name": "service foo", +// }, +// "call": {"name": "foo", "value": "bar"}, +// "params": [{"data":{"value":"foo"}, "filter": {"value":{"name":"foo","type":"bar","verboseName":"barz"}}, "type": "foo", "verboseName": "bar"}], +// "result_name": "external_service_call" +// } type CallExternalServiceAction struct { baseAction onlineAction @@ -68,6 +69,15 @@ func (a *CallExternalServiceAction) call(run flows.FlowRun, step flows.Step, ext return nil } + // substitute any variables in our params + for i, param := range params { + evaluatedParam, err := run.EvaluateTemplate(param.Data.Value) + if err != nil { + logEvent(events.NewError(err)) + } + params[i].Data.Value = evaluatedParam + } + httpLogger := &flows.HTTPLogger{} call, err := svc.Call(run.Session(), callAction, params, httpLogger.Log) From 542a2b79bc40e35e7ea5fe043b53131671aa5ae7 Mon Sep 17 00:00:00 2001 From: Robi9 Date: Tue, 4 Apr 2023 13:08:29 -0300 Subject: [PATCH 09/18] Add failed result --- flows/actions/call_external_service.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/flows/actions/call_external_service.go b/flows/actions/call_external_service.go index 184482155..8b245caea 100644 --- a/flows/actions/call_external_service.go +++ b/flows/actions/call_external_service.go @@ -89,17 +89,15 @@ func (a *CallExternalServiceAction) call(run flows.FlowRun, step flows.Step, ext } if call != nil { - if a.ResultName != "" { - input := fmt.Sprintf("%s %s", call.RequestMethod, call.RequestURL) - a.saveResult(run, step, a.ResultName, string(call.ResponseJSON), CategorySuccess, "", input, call.ResponseJSON, logEvent) - } + input := fmt.Sprintf("%s %s", call.RequestMethod, call.RequestURL) + a.saveResult(run, step, a.ResultName, string(call.ResponseJSON), CategorySuccess, "", input, call.ResponseJSON, logEvent) + } else { + a.saveResult(run, step, a.ResultName, fmt.Sprintf("%s", err), CategoryFailure, "", "", nil, logEvent) } return nil } func (a *CallExternalServiceAction) Results(include func(*flows.ResultInfo)) { - if a.ResultName != "" { - include(flows.NewResultInfo(a.ResultName, externalServiceCategories)) - } + include(flows.NewResultInfo(a.ResultName, externalServiceCategories)) } From 1065e8d2107c1eed8ade8a39575dbd80d38b6880 Mon Sep 17 00:00:00 2001 From: Rafael Soares Date: Wed, 14 Jun 2023 10:16:06 -0300 Subject: [PATCH 10/18] change externalserviceparam data.value to interface{} instead string --- assets/external_service.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/assets/external_service.go b/assets/external_service.go index fd4b50ace..1ee4cb8f6 100644 --- a/assets/external_service.go +++ b/assets/external_service.go @@ -11,11 +11,11 @@ type ExternalServiceUUID uuids.UUID // ExternalService is a third party service that can be called // -// { -// "uuid": "4e19fc3c-ae17-4f6b-acb5-7d915e29dc27", -// "name": "Third party service integration 1", -// "uuid": "generic third party service", -// } +// { +// "uuid": "4e19fc3c-ae17-4f6b-acb5-7d915e29dc27", +// "name": "Third party service integration 1", +// "uuid": "generic third party service", +// } // // @asset external_service type ExternalService interface { @@ -37,7 +37,7 @@ type ExternalServiceCallAction struct { type ExternalServiceParam struct { Data struct { - Value string `json:"value,omitempty"` + Value interface{} `json:"value,omitempty"` } `json:"data,omitempty"` Filter struct { Value *ExternalServiceFilterValue `json:"value"` @@ -53,7 +53,7 @@ type ExternalServiceFilterValue struct { } func NewExternalServiceParam( - dataValue, + dataValue interface{}, filterName, filterType, filterVerboseName, From 1f8688ed51b4a391c9ce54ba26f20b433a51b0e4 Mon Sep 17 00:00:00 2001 From: Robi9 Date: Mon, 15 May 2023 17:53:14 -0300 Subject: [PATCH 11/18] Add WithParams() for ticket triggers --- flows/triggers/base_test.go | 6 +++++ flows/triggers/ticket.go | 44 +++++++++++++++++++++---------------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/flows/triggers/base_test.go b/flows/triggers/base_test.go index 2010320c3..81ad34957 100644 --- a/flows/triggers/base_test.go +++ b/flows/triggers/base_test.go @@ -282,6 +282,12 @@ func TestTriggerMarshaling(t *testing.T) { Build(), "ticket_closed", }, + { + triggers.NewBuilder(env, flow, contact). + Ticket(ticket, triggers.TicketEventTypeClosed).WithParams(types.NewXObject(map[string]types.XValue{"foo": types.NewXText("bar")})). + Build(), + "ticket_closed", + }, } for _, tc := range triggerTests { diff --git a/flows/triggers/ticket.go b/flows/triggers/ticket.go index 1f660eb62..39e84fa2e 100644 --- a/flows/triggers/ticket.go +++ b/flows/triggers/ticket.go @@ -35,25 +35,25 @@ type TicketEvent struct { // TicketTrigger is used when a session was triggered by a ticket event // -// { -// "type": "ticket", -// "flow": {"uuid": "50c3706e-fedb-42c0-8eab-dda3335714b7", "name": "Registration"}, -// "contact": { -// "uuid": "9f7ede93-4b16-4692-80ad-b7dc54a1cd81", -// "name": "Bob", -// "created_on": "2018-01-01T12:00:00.000000Z" -// }, -// "event": { -// "type": "closed", -// "ticket": { -// "uuid": "58e9b092-fe42-4173-876c-ff45a14a24fe", -// "ticketer": {"uuid": "19dc6346-9623-4fe4-be80-538d493ecdf5", "name": "Support Tickets"}, -// "topic": {"uuid": "472a7a73-96cb-4736-b567-056d987cc5b4", "name": "Weather"}, -// "body": "Where are my shoes?" -// } -// }, -// "triggered_on": "2000-01-01T00:00:00.000000000-00:00" -// } +// { +// "type": "ticket", +// "flow": {"uuid": "50c3706e-fedb-42c0-8eab-dda3335714b7", "name": "Registration"}, +// "contact": { +// "uuid": "9f7ede93-4b16-4692-80ad-b7dc54a1cd81", +// "name": "Bob", +// "created_on": "2018-01-01T12:00:00.000000Z" +// }, +// "event": { +// "type": "closed", +// "ticket": { +// "uuid": "58e9b092-fe42-4173-876c-ff45a14a24fe", +// "ticketer": {"uuid": "19dc6346-9623-4fe4-be80-538d493ecdf5", "name": "Support Tickets"}, +// "topic": {"uuid": "472a7a73-96cb-4736-b567-056d987cc5b4", "name": "Weather"}, +// "body": "Where are my shoes?" +// } +// }, +// "triggered_on": "2000-01-01T00:00:00.000000000-00:00" +// } // // @trigger ticket type TicketTrigger struct { @@ -79,6 +79,12 @@ type TicketBuilder struct { t *TicketTrigger } +// WithParams sets the params for the trigger +func (b *TicketBuilder) WithParams(params *types.XObject) *TicketBuilder { + b.t.params = params + return b +} + // Ticket returns a ticket trigger builder func (b *Builder) Ticket(ticket *flows.Ticket, eventType TicketEventType) *TicketBuilder { return &TicketBuilder{ From 8997ed19be191b28f824a4c83c8a9d750022aed5 Mon Sep 17 00:00:00 2001 From: Robi9 Date: Wed, 17 May 2023 16:11:43 -0300 Subject: [PATCH 12/18] Remove unnecessary tests --- flows/triggers/base_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/flows/triggers/base_test.go b/flows/triggers/base_test.go index 81ad34957..2010320c3 100644 --- a/flows/triggers/base_test.go +++ b/flows/triggers/base_test.go @@ -282,12 +282,6 @@ func TestTriggerMarshaling(t *testing.T) { Build(), "ticket_closed", }, - { - triggers.NewBuilder(env, flow, contact). - Ticket(ticket, triggers.TicketEventTypeClosed).WithParams(types.NewXObject(map[string]types.XValue{"foo": types.NewXText("bar")})). - Build(), - "ticket_closed", - }, } for _, tc := range triggerTests { From 23f56642aedb647c72627b4b77128d021b61e717 Mon Sep 17 00:00:00 2001 From: Robi9 Date: Fri, 19 May 2023 10:34:26 -0300 Subject: [PATCH 13/18] Fix TestGenerateDocs --- cmd/docgen/docs/html_renderers.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/docgen/docs/html_renderers.go b/cmd/docgen/docs/html_renderers.go index e09985dd5..b75ba9345 100644 --- a/cmd/docgen/docs/html_renderers.go +++ b/cmd/docgen/docs/html_renderers.go @@ -47,6 +47,9 @@ func createItemListContextFunc(tag string, renderer renderFunc) ContextFunc { buffer := &strings.Builder{} for _, item := range items[tag] { + if len(item.examples) == 0 { + continue + } if err := renderer(buffer, item, session, voiceSession); err != nil { return nil, errors.Wrapf(err, "error rendering %s:%s", item.tagName, item.tagValue) } From f1abf35638736f97c3b971d54927fe03b32dc125 Mon Sep 17 00:00:00 2001 From: Robi9 Date: Thu, 15 Jun 2023 12:54:05 -0300 Subject: [PATCH 14/18] Add WithParams() for msg triggers --- flows/triggers/msg.go | 48 ++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/flows/triggers/msg.go b/flows/triggers/msg.go index d90cb4df4..f40c359de 100644 --- a/flows/triggers/msg.go +++ b/flows/triggers/msg.go @@ -23,27 +23,27 @@ const TypeMsg string = "msg" // MsgTrigger is used when a session was triggered by a message being received by the caller // -// { -// "type": "msg", -// "flow": {"uuid": "50c3706e-fedb-42c0-8eab-dda3335714b7", "name": "Registration"}, -// "contact": { -// "uuid": "9f7ede93-4b16-4692-80ad-b7dc54a1cd81", -// "name": "Bob", -// "created_on": "2018-01-01T12:00:00.000000Z" -// }, -// "msg": { -// "uuid": "2d611e17-fb22-457f-b802-b8f7ec5cda5b", -// "channel": {"uuid": "61602f3e-f603-4c70-8a8f-c477505bf4bf", "name": "Twilio"}, -// "urn": "tel:+12065551212", -// "text": "hi there", -// "attachments": ["https://s3.amazon.com/mybucket/attachment.jpg"] -// }, -// "keyword_match": { -// "type": "first_word", -// "keyword": "start" -// }, -// "triggered_on": "2000-01-01T00:00:00.000000000-00:00" -// } +// { +// "type": "msg", +// "flow": {"uuid": "50c3706e-fedb-42c0-8eab-dda3335714b7", "name": "Registration"}, +// "contact": { +// "uuid": "9f7ede93-4b16-4692-80ad-b7dc54a1cd81", +// "name": "Bob", +// "created_on": "2018-01-01T12:00:00.000000Z" +// }, +// "msg": { +// "uuid": "2d611e17-fb22-457f-b802-b8f7ec5cda5b", +// "channel": {"uuid": "61602f3e-f603-4c70-8a8f-c477505bf4bf", "name": "Twilio"}, +// "urn": "tel:+12065551212", +// "text": "hi there", +// "attachments": ["https://s3.amazon.com/mybucket/attachment.jpg"] +// }, +// "keyword_match": { +// "type": "first_word", +// "keyword": "start" +// }, +// "triggered_on": "2000-01-01T00:00:00.000000000-00:00" +// } // // @trigger msg type MsgTrigger struct { @@ -125,6 +125,12 @@ func (b *MsgBuilder) WithConnection(channel *assets.ChannelReference, urn urns.U return b } +// WithParams sets the params for the trigger +func (b *MsgBuilder) WithParams(params *types.XObject) *MsgBuilder { + b.t.params = params + return b +} + // Build builds the trigger func (b *MsgBuilder) Build() *MsgTrigger { return b.t From da775f034c87f9bdd3bd67fbba72eb290047d1d6 Mon Sep 17 00:00:00 2001 From: Rafael Soares Date: Wed, 14 Jun 2023 14:05:32 -0300 Subject: [PATCH 15/18] external service evaluate only string params --- flows/actions/call_external_service.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/flows/actions/call_external_service.go b/flows/actions/call_external_service.go index 8b245caea..be1000136 100644 --- a/flows/actions/call_external_service.go +++ b/flows/actions/call_external_service.go @@ -69,13 +69,16 @@ func (a *CallExternalServiceAction) call(run flows.FlowRun, step flows.Step, ext return nil } - // substitute any variables in our params + // substitute any variables in our params if data value is string for i, param := range params { - evaluatedParam, err := run.EvaluateTemplate(param.Data.Value) - if err != nil { - logEvent(events.NewError(err)) + dataValue, ok := param.Data.Value.(string) + if ok { + evaluatedParam, err := run.EvaluateTemplate(dataValue) + if err != nil { + logEvent(events.NewError(err)) + } + params[i].Data.Value = evaluatedParam } - params[i].Data.Value = evaluatedParam } httpLogger := &flows.HTTPLogger{} From eda438242093e0d8719fe45d03e9666e0a9a58e5 Mon Sep 17 00:00:00 2001 From: Rafael Soares Date: Wed, 21 Jun 2023 19:03:18 -0300 Subject: [PATCH 16/18] avoid panic if reflect type kind is interface and theres is no a underlying type --- flows/inspect/reflect.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flows/inspect/reflect.go b/flows/inspect/reflect.go index c0a06e720..170d6555f 100644 --- a/flows/inspect/reflect.go +++ b/flows/inspect/reflect.go @@ -160,6 +160,9 @@ func walkTypes(t reflect.Type, path string, visitField func(string, *EngineField // gets the actual type if we've been given an interface or pointer type func derefType(t reflect.Type) reflect.Type { for t.Kind() == reflect.Interface || t.Kind() == reflect.Ptr { + if t.Kind() == reflect.Interface { // avoid panic if reflect type kind is interface and theres is no a underlying type + return t + } t = t.Elem() } return t From 7bdab51e035ea30ceeb53df27e88b514dafa853d Mon Sep 17 00:00:00 2001 From: Rafael Soares Date: Wed, 21 Jun 2023 19:08:07 -0300 Subject: [PATCH 17/18] update ci codecov-action to v3 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0d78b554..4d31ad216 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - name: Upload coverage if: success() - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 with: fail_ci_if_error: true From f84288cbc8adc6848c015350540eafb552a78264 Mon Sep 17 00:00:00 2001 From: Rafael Soares Date: Wed, 21 Jun 2023 19:19:58 -0300 Subject: [PATCH 18/18] create weni-changelog.md for 0.1.0-goflow-0.144.3 --- WENI-CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 WENI-CHANGELOG.md diff --git a/WENI-CHANGELOG.md b/WENI-CHANGELOG.md new file mode 100644 index 000000000..9da43c294 --- /dev/null +++ b/WENI-CHANGELOG.md @@ -0,0 +1,5 @@ +0.1.0-goflow-0.144.3 +---------- + * Add support for external services actions and events + * Add Support for trigger.params in Msg events + * Add support for trigger.params in ticket events \ No newline at end of file