Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions packages/engine/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ require (
github.com/jackc/pgx/v5 v5.8.0
github.com/pressly/goose/v3 v3.27.0
github.com/prometheus/client_golang v1.23.2
github.com/rabbitmq/amqp091-go v1.11.0
github.com/redis/go-redis/v9 v9.20.0
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/testcontainers/testcontainers-go v0.41.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.41.0
go.mongodb.org/mongo-driver/v2 v2.6.0
golang.org/x/oauth2 v0.35.0
golang.org/x/term v0.41.0
golang.org/x/time v0.15.0
Expand Down Expand Up @@ -126,7 +129,6 @@ require (
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/redis/go-redis/v9 v9.20.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
Expand All @@ -141,8 +143,11 @@ require (
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver/v2 v2.6.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
Expand Down
19 changes: 19 additions & 0 deletions packages/engine/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
Expand Down Expand Up @@ -212,6 +216,8 @@ github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRt
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
Expand Down Expand Up @@ -285,6 +291,8 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rabbitmq/amqp091-go v1.11.0 h1:HxIctVm9Gid/Vtn706necmZ7Wj6pgGI2eqplRbEY8O8=
github.com/rabbitmq/amqp091-go v1.11.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
github.com/redis/go-redis/v9 v9.20.0 h1:WnQYxLkgO2xiXTCJY0ldIiI8dNqCDlQAG+AtaH7a2a0=
github.com/redis/go-redis/v9 v9.20.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
Expand Down Expand Up @@ -339,9 +347,19 @@ github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9R
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8=
go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
Expand Down Expand Up @@ -424,6 +442,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
Expand Down
16 changes: 16 additions & 0 deletions packages/engine/internal/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ func NewRegistry() *Registry {
r.Register("redis/publish", &RedisPublishConnector{})
r.Register("mongodb/find", &MongoFindConnector{})
r.Register("mongodb/aggregate", &MongoAggregateConnector{})
r.Register("stripe/create_charge", &StripeCreateChargeConnector{})
r.Register("stripe/create_customer", &StripeCreateCustomerConnector{})
r.Register("stripe/create_refund", &StripeCreateRefundConnector{})
r.Register("okta/list_users", &OktaListUsersConnector{})
r.Register("okta/create_user", &OktaCreateUserConnector{})
r.Register("quickbooks/create_invoice", &QuickBooksCreateInvoiceConnector{})
r.Register("quickbooks/list_invoices", &QuickBooksListInvoicesConnector{})
r.Register("onedrive/upload", &OneDriveUploadConnector{})
r.Register("sharepoint/list_items", &SharePointListItemsConnector{})
r.Register("rabbitmq/publish", &RabbitMQPublishConnector{})
r.Register("rabbitmq/consume", &RabbitMQConsumeConnector{})
r.Register("shopify/list_orders", &ShopifyListOrdersConnector{})
r.Register("shopify/list_products", &ShopifyListProductsConnector{})
r.Register("shopify/create_order", &ShopifyCreateOrderConnector{})
r.Register("mailchimp/list_members", &MailchimpListMembersConnector{})
r.Register("mailchimp/add_member", &MailchimpAddMemberConnector{})
return r
}

Expand Down
186 changes: 186 additions & 0 deletions packages/engine/internal/connector/mailchimp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package connector

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)

// MailchimpListMembersConnector lists members of a Mailchimp audience list.
type MailchimpListMembersConnector struct {
Client *http.Client
baseURL string
}

func (c *MailchimpListMembersConnector) apiURL(dc, path string) string {
if c.baseURL != "" {
return c.baseURL + path
}
return fmt.Sprintf("https://%s.api.mailchimp.com/3.0%s", dc, path)
}

func (c *MailchimpListMembersConnector) Execute(ctx context.Context, params map[string]any) (map[string]any, error) {
dc, apiKey, err := extractMailchimpCredential(params)
if err != nil {
return nil, fmt.Errorf("mailchimp/list_members: %w", err)
}

listID, _ := params["list_id"].(string)
if listID == "" {
return nil, fmt.Errorf("mailchimp/list_members: list_id is required")
}

endpoint := c.apiURL(dc, fmt.Sprintf("/lists/%s/members", listID))
if count, ok := extractInt(params["count"]); ok && count > 0 {
endpoint += fmt.Sprintf("?count=%d", count)
}

req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
if err != nil {
return nil, fmt.Errorf("mailchimp/list_members: creating request: %w", err)
}
req.SetBasicAuth("anystring", apiKey)

resp, err := httpClient(c.Client).Do(req)
if err != nil {
return nil, fmt.Errorf("mailchimp/list_members: %w", err)
}
defer resp.Body.Close()

body, err := io.ReadAll(io.LimitReader(resp.Body, DefaultMaxResponseBytes))
if err != nil {
return nil, fmt.Errorf("mailchimp/list_members: reading response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("mailchimp/list_members: Mailchimp API returned %d: %s", resp.StatusCode, truncate(string(body), 500))
}

var result struct {
Members []any `json:"members"`
TotalItems int `json:"total_items"`
}
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("mailchimp/list_members: parsing response: %w", err)
}
return map[string]any{
"members": result.Members,
"count": len(result.Members),
"total_items": result.TotalItems,
}, nil
}

// MailchimpAddMemberConnector adds or updates a subscriber in a Mailchimp audience list.
type MailchimpAddMemberConnector struct {
Client *http.Client
baseURL string
}

func (c *MailchimpAddMemberConnector) apiURL(dc, path string) string {
if c.baseURL != "" {
return c.baseURL + path
}
return fmt.Sprintf("https://%s.api.mailchimp.com/3.0%s", dc, path)
}

func (c *MailchimpAddMemberConnector) Execute(ctx context.Context, params map[string]any) (map[string]any, error) {
dc, apiKey, err := extractMailchimpCredential(params)
if err != nil {
return nil, fmt.Errorf("mailchimp/add_member: %w", err)
}

listID, _ := params["list_id"].(string)
if listID == "" {
return nil, fmt.Errorf("mailchimp/add_member: list_id is required")
}
email, _ := params["email"].(string)
if email == "" {
return nil, fmt.Errorf("mailchimp/add_member: email is required")
}

status := "subscribed"
if s, ok := params["status"].(string); ok && s != "" {
status = s
}

member := map[string]any{
"email_address": email,
"status": status,
}
if mergeFields, ok := params["merge_fields"].(map[string]any); ok {
member["merge_fields"] = mergeFields
}
if tags, ok := params["tags"].([]any); ok && len(tags) > 0 {
member["tags"] = tags
}

reqJSON, err := json.Marshal(member)
if err != nil {
return nil, fmt.Errorf("mailchimp/add_member: marshaling request: %w", err)
}

req, err := http.NewRequestWithContext(ctx, "POST",
c.apiURL(dc, fmt.Sprintf("/lists/%s/members", listID)),
bytes.NewReader(reqJSON),
)
if err != nil {
return nil, fmt.Errorf("mailchimp/add_member: creating request: %w", err)
}
req.SetBasicAuth("anystring", apiKey)
req.Header.Set("Content-Type", "application/json")

resp, err := httpClient(c.Client).Do(req)
if err != nil {
return nil, fmt.Errorf("mailchimp/add_member: %w", err)
}
defer resp.Body.Close()

body, err := io.ReadAll(io.LimitReader(resp.Body, DefaultMaxResponseBytes))
if err != nil {
return nil, fmt.Errorf("mailchimp/add_member: reading response: %w", err)
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("mailchimp/add_member: Mailchimp API returned %d: %s", resp.StatusCode, truncate(string(body), 500))
}

return parseJSONBody(body, "mailchimp/add_member")
}

func extractMailchimpCredential(params map[string]any) (dc, apiKey string, err error) {
raw, ok := params["_credential"]
if !ok || raw == nil {
return "", "", fmt.Errorf("credential is required")
}
delete(params, "_credential")

var cred map[string]string
switch v := raw.(type) {
case map[string]string:
cred = v
case map[string]any:
cred = make(map[string]string, len(v))
for k, val := range v {
if s, ok := val.(string); ok {
cred[k] = s
}
}
default:
return "", "", fmt.Errorf("credential is required")
}

apiKey = cred["api_key"]
if apiKey == "" {
return "", "", fmt.Errorf("credential must contain an 'api_key' field")
}

// Data center is the suffix after the last '-' in the API key (e.g. "abc123-us1" → "us1").
parts := strings.Split(apiKey, "-")
if len(parts) < 2 {
return "", "", fmt.Errorf("credential api_key format invalid: expected '<key>-<dc>' (e.g. abc123-us1)")
}
dc = parts[len(parts)-1]
return dc, apiKey, nil
}
Loading
Loading