Repository: amp-labs/connectors
Pull request #321 (2024-05-13 23:19:45)

feat: introduce `displayName` to catalog
## Checklist
- [x] Ran Linter
- [x] Catalog tests passing

This PR introduces a `displayName` field to ProviderInfo and adds display names for Zendesk Chat and Zendesk Support



Patch:
diff --git a/go.mod b/go.mod
index 6fcbe969..cdadfd72 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/amp-labs/connectors
 
-go 1.21
+go 1.21.0
 
 require (
 	github.com/brianvoe/gofakeit/v6 v6.28.0
diff --git a/outreach/README.md b/outreach/README.md
index 22145e31..74de52b9 100644
--- a/outreach/README.md
+++ b/outreach/README.md
@@ -14,11 +14,53 @@ Read is used to list all records of a given type. For example, if you want to li
 // Call Read to list records in a users object
 res, err := client.Read(context.TODO(),common.ReadParams{
 		ObjectName: "users",
-        })
+})
 if err != nil {
 	log.Fatal(err)
 }
 
 ```
 
+## Write
+Write is used to create/update objects in outreach connector. For an instance creating an emailAddress object, you use the Write method and `emailAddress` object   
+
+### Example usage
+```
+
+type Attribute struct {
+	Email     string `json:"email"`
+	EmailType string `json:"emailType"`
+	Order     int    `json:"order"`
+	Status    string `json:"status"`
+}
+
+type EmailAddress struct {
+	Attributes Attribute `json:"attributes"`
+	Type       string    `json:"type"`
+}
+
+// Create an outreach connector instance (any method you prefer)
+// This Assumes you named the instance client
+
+attribute := Attribute{
+		Email:     gofakeit.Email(),
+		EmailType: "email",
+		Order:     0,
+		Status:    "null",
+	}
+
+	config := common.WriteParams{
+		ObjectName: "emailAddresses",
+		RecordData: EmailAddress{
+			Attributes: attribute,
+			Type:       "emailAddress",
+		},
+	}
+
+result, err := client.Write(ctx, config)
+if err != nil {
+	log.Fatal(err)
+}
+```
+
 
diff --git a/outreach/write.go b/outreach/write.go
new file mode 100644
index 00000000..5d2172a0
--- /dev/null
+++ b/outreach/write.go
@@ -0,0 +1,63 @@
+package outreach
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/url"
+
+	"github.com/amp-labs/connectors/common"
+)
+
+type WriteResponse struct {
+	Data map[string]any `json:"data"`
+}
+
+var JSONAPIContentTypeHeader = common.Header{ //nolint:gochecknoglobals
+	Key:   "Content-Type",
+	Value: "application/vnd.api+json",
+}
+
+type writeMethod func(context.Context, string, any, ...common.Header) (*common.JSONHTTPResponse, error)
+
+func (c *Connector) Write(ctx context.Context, config common.WriteParams) (*common.WriteResult, error) {
+	var write writeMethod
+
+	URL, err := url.JoinPath(c.BaseURL, config.ObjectName)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(config.RecordId) > 0 {
+		URL, err = url.JoinPath(URL, config.RecordId)
+		if err != nil {
+			return nil, err
+		}
+
+		write = c.Client.Patch
+	} else {
+		write = c.Client.Post
+	}
+
+	// Outreach expects everything to be wrapped in a "data" object.
+	data := make(map[string]any)
+	data["data"] = config.RecordData
+
+	res, err := write(ctx, URL, data, JSONAPIContentTypeHeader)
+	if err != nil {
+		return nil, err
+	}
+
+	var response WriteResponse
+
+	err = json.Unmarshal(res.Body.Source(), &response)
+	if err != nil {
+		return nil, err
+	}
+
+	return &common.WriteResult{
+		Success:  true,
+		RecordId: fmt.Sprint(response.Data["id"]),
+		Data:     response.Data,
+	}, nil
+}
diff --git a/providers/catalog.go b/providers/catalog.go
index e5dd15c0..e2ccaea5 100644
--- a/providers/catalog.go
+++ b/providers/catalog.go
@@ -64,6 +64,7 @@ const (
 	ZohoCRM                             Provider = "zohoCRM"
 	Zoom                                Provider = "zoom"
 	Zuora                               Provider = "zuora"
+	Aircall                             Provider = "aircall"
 )
 
 // ================================================================================
@@ -178,7 +179,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 				Upsert: false,
 				Delete: false,
 			},
-			Proxy:     false,
+			Proxy:     true,
 			Read:      false,
 			Subscribe: false,
 			Write:     false,
@@ -921,7 +922,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 				Upsert: false,
 				Delete: false,
 			},
-			Proxy:     false,
+			Proxy:     true,
 			Read:      false,
 			Subscribe: false,
 			Write:     false,
@@ -930,8 +931,9 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 
 	// Zendesk Support configuration
 	ZendeskSupport: {
-		AuthType: Oauth2,
-		BaseURL:  "https://{{.workspace}}.zendesk.com",
+		DisplayName: "Zendesk Support",
+		AuthType:    Oauth2,
+		BaseURL:     "https://{{.workspace}}.zendesk.com",
 		OauthOpts: OauthOpts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://{{.workspace}}.zendesk.com/oauth/authorizations/new",
@@ -954,8 +956,9 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	},
 
 	ZendeskChat: {
-		AuthType: Oauth2,
-		BaseURL:  "https://www.zopim.com",
+		DisplayName: "Zendesk Chat",
+		AuthType:    Oauth2,
+		BaseURL:     "https://www.zopim.com",
 		OauthOpts: OauthOpts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://www.zopim.com/oauth2/authorizations/new?subdomain={{.workspace}}",
@@ -1592,4 +1595,32 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 			Write:     false,
 		},
 	},
+
+	// Aircall Configuration
+	Aircall: {
+		AuthType: Oauth2,
+		BaseURL:  "https://api.aircall.io",
+		OauthOpts: OauthOpts{
+			GrantType:                 AuthorizationCode,
+			AuthURL:                   "https://dashboard.aircall.io/oauth/authorize",
+			TokenURL:                  "https://api.aircall.io/v1/oauth/token",
+			ExplicitScopesRequired:    true,
+			ExplicitWorkspaceRequired: false,
+			TokenMetadataFields: TokenMetadataFields{
+				ScopesField: "scope",
+			},
+		},
+		Support: Support{
+			BulkWrite: BulkWriteSupport{
+				Insert: false,
+				Update: false,
+				Upsert: false,
+				Delete: false,
+			},
+			Proxy:     false,
+			Read:      false,
+			Subscribe: false,
+			Write:     false,
+		},
+	},
 }
diff --git a/providers/types.gen.go b/providers/types.gen.go
index bdbb7b07..a5408bca 100644
--- a/providers/types.gen.go
+++ b/providers/types.gen.go
@@ -48,8 +48,11 @@ type Provider = string
 
 // ProviderInfo defines model for ProviderInfo.
 type ProviderInfo struct {
-	AuthType     AuthType     `json:"authType" validate:"required"`
-	BaseURL      string       `json:"baseURL" validate:"required"`
+	AuthType AuthType `json:"authType" validate:"required"`
+	BaseURL  string   `json:"baseURL" validate:"required"`
+
+	// DisplayName The display name of the provider, if omitted, defaults to provider name.
+	DisplayName  string       `json:"displayName,omitempty"`
 	OauthOpts    OauthOpts    `json:"oauthOpts" validate:"required"`
 	ProviderOpts ProviderOpts `json:"providerOpts"`
 	Support      Support      `json:"support" validate:"required"`
diff --git a/providers/types.yaml b/providers/types.yaml
index b3fe8e31..a345fe05 100644
--- a/providers/types.yaml
+++ b/providers/types.yaml
@@ -139,7 +139,11 @@ components:
           $ref: '#/components/schemas/Support'
         providerOpts:
           $ref: '#/components/schemas/ProviderOpts'
-
+        displayName:
+          type: string
+          example: Zendesk Chat
+          description: The display name of the provider, if omitted, defaults to provider name.
+          x-go-type-skip-optional-pointer: true
     CatalogType:
       type: object
       additionalProperties:


Test patch:
diff --git a/providers/catalog_test.go b/providers/catalog_test.go
index 175dd446..70f5c0d4 100644
--- a/providers/catalog_test.go
+++ b/providers/catalog_test.go
@@ -145,7 +145,7 @@ var testCases = []struct { // nolint
 					Delete: false,
 				},
 				Subscribe: false,
-				Proxy:     false,
+				Proxy:     true,
 			},
 			AuthType: Oauth2,
 			OauthOpts: OauthOpts{
@@ -1008,7 +1008,7 @@ var testCases = []struct { // nolint
 					Delete: false,
 				},
 				Subscribe: false,
-				Proxy:     false,
+				Proxy:     true,
 			},
 			AuthType: Oauth2,
 			OauthOpts: OauthOpts{
@@ -1783,6 +1783,37 @@ var testCases = []struct { // nolint
 		},
 		expectedErr: nil,
 	},
+	{
+		provider:    Aircall,
+		description: "Valid Aircall provider config without substitutions",
+		expected: &ProviderInfo{
+			AuthType: Oauth2,
+			OauthOpts: OauthOpts{
+				GrantType:                 AuthorizationCode,
+				AuthURL:                   "https://dashboard.aircall.io/oauth/authorize",
+				TokenURL:                  "https://api.aircall.io/v1/oauth/token",
+				ExplicitScopesRequired:    true,
+				ExplicitWorkspaceRequired: false,
+				TokenMetadataFields: TokenMetadataFields{
+					ScopesField: "scope",
+				},
+			},
+			Support: Support{
+				BulkWrite: BulkWriteSupport{
+					Insert: false,
+					Update: false,
+					Upsert: false,
+					Delete: false,
+				},
+				Proxy:     false,
+				Read:      false,
+				Subscribe: false,
+				Write:     false,
+			},
+			BaseURL: "https://api.aircall.io",
+		},
+		expectedErr: nil,
+	},
 }
 
 func TestReadInfo(t *testing.T) { // nolint
diff --git a/test/outreach/read-write/read-write.go b/test/outreach/read-write/read-write.go
new file mode 100644
index 00000000..9f79a602
--- /dev/null
+++ b/test/outreach/read-write/read-write.go
@@ -0,0 +1,204 @@
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"log"
+	"net/http"
+	"os"
+
+	"github.com/amp-labs/connectors"
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/outreach"
+	testUtils "github.com/amp-labs/connectors/test/utils"
+	"github.com/amp-labs/connectors/utils"
+	"github.com/brianvoe/gofakeit/v6"
+)
+
+const (
+	DefaultCredsFile = "creds.json"
+)
+
+type Attribute struct {
+	Email     string `json:"email"`
+	EmailType string `json:"emailType"`
+	Order     int    `json:"order"`
+	Status    string `json:"status"`
+}
+
+type EmailAddress struct {
+	Attributes Attribute `json:"attributes"`
+	Type       string    `json:"type"`
+}
+
+type EmailAddressUpdate struct {
+	Attributes Attribute `json:"attributes"`
+	Type       string    `json:"type"`
+	ID         int       `json:"id"` // necessary in updating
+}
+
+func GetOutreachConnector(ctx context.Context, filePath string) *outreach.Connector {
+	registry := utils.NewCredentialsRegistry()
+
+	readers := []utils.Reader{
+		&utils.JSONReader{
+			FilePath: filePath,
+			JSONPath: "$['clientId']",
+			CredKey:  "clientId",
+		},
+		&utils.JSONReader{
+			FilePath: filePath,
+			JSONPath: "$['clientSecret']",
+			CredKey:  "clientSecret",
+		},
+		&utils.JSONReader{
+			FilePath: filePath,
+			JSONPath: "$['refreshToken']",
+			CredKey:  "refreshToken",
+		},
+		&utils.JSONReader{
+			FilePath: filePath,
+			JSONPath: "$['accessToken']",
+			CredKey:  "accessToken",
+		},
+		&utils.JSONReader{
+			FilePath: filePath,
+			JSONPath: "$['provider']",
+			CredKey:  "provider",
+		},
+	}
+	registry.AddReaders(readers...)
+
+	cfg := utils.OutreachOAuthConfigFromRegistry(registry)
+	tok := utils.OutreachOauthTokenFromRegistry(registry)
+
+	conn, err := connectors.Outreach(
+		outreach.WithClient(ctx, http.DefaultClient, cfg, tok),
+	)
+	if err != nil {
+		testUtils.Fail("error creating outreach connector", "error", err)
+	}
+
+	return conn
+}
+
+func main() {
+	var err error
+
+	outreach := GetOutreachConnector(context.Background(), DefaultCredsFile)
+
+	err = testReadConnector(context.Background(), outreach)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	err = testWriteConnector(context.Background(), outreach)
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+func testReadConnector(ctx context.Context, conn connectors.ReadConnector) error {
+	config := connectors.ReadParams{
+		ObjectName: "users",
+		// NextPage:   "https://api.outreach.io/api/v2/users?page%5Blimit%5D=1\u0026page%5Boffset%5D=2",
+	}
+
+	result, err := conn.Read(ctx, config)
+	if err != nil {
+		return err
+	}
+
+	// Print the results
+	jsonStr, err := json.MarshalIndent(result, "", "  ")
+	if err != nil {
+		return err
+	}
+
+	_, _ = os.Stdout.Write(jsonStr)
+	_, _ = os.Stdout.WriteString("\n")
+
+	return nil
+}
+
+func testWriteConnector(ctx context.Context, conn connectors.WriteConnector) error {
+	var err error
+
+	err = testCreate(ctx, conn)
+	if err != nil {
+		return err
+	}
+
+	err = testUpdate(ctx, conn)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func testCreate(ctx context.Context, conn connectors.WriteConnector) error {
+	attribute := Attribute{
+		Email:     gofakeit.Email(),
+		EmailType: "email",
+		Order:     0,
+		Status:    "null",
+	}
+
+	config := common.WriteParams{
+		ObjectName: "emailAddresses",
+		RecordData: EmailAddress{
+			Attributes: attribute,
+			Type:       "emailAddress",
+		},
+	}
+
+	result, err := conn.Write(ctx, config)
+	if err != nil {
+		return err
+	}
+
+	// Print the results
+	jsonStr, err := json.MarshalIndent(result, "", "  ")
+	if err != nil {
+		return err
+	}
+
+	_, _ = os.Stdout.Write(jsonStr)
+	_, _ = os.Stdout.WriteString("\n")
+
+	return nil
+}
+
+func testUpdate(ctx context.Context, conn connectors.WriteConnector) error {
+	patt := Attribute{
+		Email: "groverstiedemann@lehner.io",
+		Order: gofakeit.Number(0, 10),
+	}
+
+	config := common.WriteParams{
+		ObjectName: "emailAddresses",
+		RecordId:   "5",
+		RecordData: EmailAddressUpdate{
+			Attributes: patt,
+			ID:         5,
+			Type:       "emailAddress",
+		},
+	}
+
+	result, err := conn.Write(ctx, config)
+	if err != nil {
+		return err
+	}
+
+	// Print the results
+	jsonStr, err := json.MarshalIndent(result, "", "  ")
+	if err != nil {
+		return err
+	}
+
+	_, _ = os.Stdout.Write(jsonStr)
+	_, _ = os.Stdout.WriteString("\n")
+
+	return nil
+}
diff --git a/test/outreach/read/read.go b/test/outreach/read/read.go
deleted file mode 100644
index 0dc8021d..00000000
--- a/test/outreach/read/read.go
+++ /dev/null
@@ -1,86 +0,0 @@
-package main
-
-import (
-	"context"
-	"encoding/json"
-	"log"
-	"net/http"
-	"os"
-
-	"github.com/amp-labs/connectors"
-	"github.com/amp-labs/connectors/outreach"
-	testUtils "github.com/amp-labs/connectors/test/utils"
-	"github.com/amp-labs/connectors/utils"
-)
-
-const (
-	DefaultCredsFile = "creds.json"
-)
-
-func GetOutreachConnector(ctx context.Context, filePath string) *outreach.Connector {
-	registry := utils.NewCredentialsRegistry()
-
-	readers := []utils.Reader{
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$['clientId']",
-			CredKey:  "clientId",
-		},
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$['clientSecret']",
-			CredKey:  "clientSecret",
-		},
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$['refreshToken']",
-			CredKey:  "refreshToken",
-		},
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$['accessToken']",
-			CredKey:  "accessToken",
-		},
-		&utils.JSONReader{
-			FilePath: filePath,
-			JSONPath: "$['provider']",
-			CredKey:  "provider",
-		},
-	}
-	registry.AddReaders(readers...)
-
-	cfg := utils.OutreachOAuthConfigFromRegistry(registry)
-	tok := utils.OutreachOauthTokenFromRegistry(registry)
-
-	conn, err := connectors.Outreach(
-		outreach.WithClient(ctx, http.DefaultClient, cfg, tok),
-	)
-	if err != nil {
-		testUtils.Fail("error creating outreach connector", "error", err)
-	}
-
-	return conn
-}
-
-func main() {
-	outreach := GetOutreachConnector(context.Background(), DefaultCredsFile)
-
-	config := connectors.ReadParams{
-		ObjectName: "users",
-		// NextPage:   "https://api.outreach.io/api/v2/users?page%5Blimit%5D=1\u0026page%5Boffset%5D=2",
-	}
-
-	result, err := outreach.Read(context.Background(), config)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	// Print the results
-	jsonStr, err := json.MarshalIndent(result, "", "  ")
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	_, _ = os.Stdout.Write(jsonStr)
-	_, _ = os.Stdout.WriteString("\n")
-}