Repository: amp-labs/connectors
Pull request #103 (2024-03-18 13:49:03)

[ENG-798] feat: Add Pipedrive connector
 + Ran Linter
 + Catalog tests passing
 + Created PR from non-main branch

## Catalog variables
No required catalog variables

## Notes

## Testing 
### GET 
URL: <localhost:4444/v1/deals> 
![proxy get](https://github.com/amp-labs/connectors/assets/95291462/af62a96e-a29c-4859-9ff2-f6ad3a0650fc)


### POST
URL: <localhost:4444/v1/deals>
![proxy post](https://github.com/amp-labs/connectors/assets/95291462/69e44010-efb0-45f7-b659-bbd9bc410d0e)


### PUT
URL: <localhost:4444/v1/deals/2>
![proxy put](https://github.com/amp-labs/connectors/assets/95291462/4076fb85-9fc6-4b83-b5b2-209284d39e2e)

### DELETE
URL: <api.pipedrive.com/v1/deals/1> - even the direct del doesn't work
![delete doesnt work](https://github.com/amp-labs/connectors/assets/95291462/7123b1a6-f4fc-4bba-979c-09f923e47abb)
Although the scopes contain "base,deals:full" permission. And the description of this scope tells that it is possibly to read (works), update (works) and delete (doesn't). Have raised this as an issue at the dev forum - https://devcommunity.pipedrive.com/t/deal-inserts-and-updates-work-fine-via-api-but-delete-fails/7977

## Pagination
![pagination_example](https://github.com/amp-labs/connectors/assets/95291462/c2ea10c6-feca-4ccf-8764-bcbbb1aaf20c)


Listener430: @RajatPawar FYI

Patch:
diff --git a/basic/string.go b/basic/string.go
index 6d102344..e38aad4e 100644
--- a/basic/string.go
+++ b/basic/string.go
@@ -1,9 +1,5 @@
 package basic
 
-import (
-	"fmt"
-)
-
 func (c *Connector) String() string {
-	return fmt.Sprintf("%s.Connector", c.Provider())
+	return c.Provider() + ".Connector"
 }
diff --git a/common/oauth.go b/common/oauth.go
index 18217df3..52561205 100644
--- a/common/oauth.go
+++ b/common/oauth.go
@@ -26,8 +26,8 @@ func NewOAuthHTTPClient(ctx context.Context, opts ...OAuthOption) (Authenticated
 	}
 
 	var err error
-	params, err = params.prepare()
 
+	params, err = params.prepare()
 	if err != nil {
 		return nil, err
 	}
diff --git a/go.mod b/go.mod
index 51344f73..98da7900 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
 	github.com/brianvoe/gofakeit/v6 v6.28.0
 	github.com/go-playground/validator v9.31.0+incompatible
 	github.com/joho/godotenv v1.5.1
-	github.com/spyzhov/ajson v0.9.0
+	github.com/spyzhov/ajson v0.9.1
 	github.com/stretchr/testify v1.9.0
 	github.com/subchen/go-xmldom v1.1.2
 	golang.org/x/oauth2 v0.18.0
diff --git a/go.sum b/go.sum
index fa6a8675..7f0999b8 100644
--- a/go.sum
+++ b/go.sum
@@ -23,8 +23,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/spyzhov/ajson v0.9.0 h1:tF46gJGOenYVj+k9K1U1XpCxVWhmiyY5PsVCAs1+OJ0=
-github.com/spyzhov/ajson v0.9.0/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8=
+github.com/spyzhov/ajson v0.9.1 h1:izsDkOC9yznlpA2BxJcrkr8Q5Gyjf43yeSc2Wz0g/aU=
+github.com/spyzhov/ajson v0.9.1/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8=
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subchen/go-xmldom v1.1.2 h1:7evI2YqfYYOnuj+PBwyaOZZYjl3iWq35P6KfBUw9jeU=
diff --git a/providers/catalog.go b/providers/catalog.go
index 866ed756..01fe8e86 100644
--- a/providers/catalog.go
+++ b/providers/catalog.go
@@ -10,6 +10,7 @@ const (
 	LinkedIn   Provider = "linkedIn"
 	Salesloft  Provider = "salesloft"
 	Outreach   Provider = "outreach"
+	Pipedrive  Provider = "pipedrive"
 	Sellsy     Provider = "sellsy"
 	Attio      Provider = "attio"
 	Close      Provider = "close"
@@ -132,6 +133,25 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 		},
 	},
 
+	// Pipedrive configuration
+	Pipedrive: {
+		AuthType: Oauth2,
+		BaseURL:  "https://api.pipedrive.com",
+		OauthOpts: OauthOpts{
+			AuthURL:                   "https://oauth.pipedrive.com/oauth/authorize",
+			TokenURL:                  "https://oauth.pipedrive.com/oauth/token",
+			ExplicitScopesRequired:    true,
+			ExplicitWorkspaceRequired: false,
+		},
+		Support: Support{
+			BulkWrite: false,
+			Proxy:     false,
+			Read:      false,
+			Subscribe: false,
+			Write:     false,
+		},
+	},
+
 	// Sellsy configuration
 	Sellsy: {
 		AuthType: Oauth2,
diff --git a/salesforce/bulkwrite.go b/salesforce/bulkwrite.go
index 5ad2bdef..96d88e77 100644
--- a/salesforce/bulkwrite.go
+++ b/salesforce/bulkwrite.go
@@ -248,7 +248,7 @@ func (c *Connector) completeUpload(ctx context.Context, jobId string) (*common.J
 		"state": JobStateUploadComplete,
 	}
 
-	location, err := joinURLPath(c.BaseURL, fmt.Sprintf("jobs/ingest/%s", jobId))
+	location, err := joinURLPath(c.BaseURL, "jobs/ingest/"+jobId)
 	if err != nil {
 		return nil, err
 	}
diff --git a/salesforce/read.go b/salesforce/read.go
index 89225638..d2979bb0 100644
--- a/salesforce/read.go
+++ b/salesforce/read.go
@@ -20,7 +20,7 @@ func (c *Connector) Read(ctx context.Context, config common.ReadParams) (*common
 	if len(config.NextPage) > 0 {
 		// If NextPage is set, then we're reading the next page of results.
 		// All that matters is the NextPage URL, the fields are ignored.
-		location, joinErr := url.JoinPath(fmt.Sprintf("https://%s", c.Domain), config.NextPage)
+		location, joinErr := url.JoinPath("https://"+c.Domain, config.NextPage)
 		if joinErr != nil {
 			return nil, joinErr
 		}
@@ -75,7 +75,7 @@ func makeSOQL(config common.ReadParams) (string, error) {
 
 	// If Since is not set, then we're doing a backfill. We read all rows (in pages)
 	if !config.Since.IsZero() {
-		soql += fmt.Sprintf(" WHERE SystemModstamp > %s", config.Since.Format("2006-01-02T15:04:05Z"))
+		soql += " WHERE SystemModstamp > " + config.Since.Format("2006-01-02T15:04:05Z")
 		hasWhere = true
 	}
 
diff --git a/salesforce/write.go b/salesforce/write.go
index 6a0ec6c6..3e70c556 100644
--- a/salesforce/write.go
+++ b/salesforce/write.go
@@ -2,7 +2,6 @@ package salesforce
 
 import (
 	"context"
-	"fmt"
 	"net/url"
 
 	"github.com/amp-labs/connectors/common"
@@ -16,7 +15,7 @@ func (c *Connector) Write(ctx context.Context, config common.WriteParams) (*comm
 		err error
 	)
 
-	location, joinErr := url.JoinPath(fmt.Sprintf("%s/sobjects", c.BaseURL), config.ObjectName)
+	location, joinErr := url.JoinPath(c.BaseURL+"/sobjects", config.ObjectName)
 	if joinErr != nil {
 		return nil, joinErr
 	}


Test patch:
diff --git a/providers/catalog_test.go b/providers/catalog_test.go
index a7b0f294..3a633c99 100644
--- a/providers/catalog_test.go
+++ b/providers/catalog_test.go
@@ -158,6 +158,28 @@ var testCases = []struct { // nolint
 		expectedErr: nil,
 	},
 
+	{
+		provider: Pipedrive,
+		expected: &ProviderInfo{
+			AuthType: Oauth2,
+			OauthOpts: OauthOpts{
+				AuthURL:                   "https://oauth.pipedrive.com/oauth/authorize",
+				TokenURL:                  "https://oauth.pipedrive.com/oauth/token",
+				ExplicitScopesRequired:    true,
+				ExplicitWorkspaceRequired: false,
+			},
+			Support: Support{
+				BulkWrite: false,
+				Proxy:     false,
+				Read:      false,
+				Subscribe: false,
+				Write:     false,
+			},
+			BaseURL: "https://api.pipedrive.com",
+		},
+		expectedErr: nil,
+	},
+
 	{
 		provider: Sellsy,
 		expected: &ProviderInfo{
diff --git a/test/metadata/main.go b/test/metadata/main.go
index ac930524..f002d487 100644
--- a/test/metadata/main.go
+++ b/test/metadata/main.go
@@ -66,7 +66,6 @@ func main() {
 	}()
 
 	example, err := xmldom.ParseXML(`<createMetadata><metadata xsi:type="CustomObject"><fullName>TestObject15__c</fullName><label>Test Object 15</label><pluralLabel>Test Objects 15</pluralLabel><nameField><type>Text</type><label>Test Object Name</label></nameField><deploymentStatus>Deployed</deploymentStatus><sharingModel>ReadWrite</sharingModel></metadata><metadata xsi:type="CustomField"><fullName>TestObject13__c.Comments__c</fullName><label>Comments</label><type>LongTextArea</type><length>500</length><inlineHelpText>This field contains help text for this object</inlineHelpText><description>Add your comments about this object here</description><visibleLines>30</visibleLines><required>false</required><trackFeedHistory>false</trackFeedHistory><trackHistory>false</trackHistory></metadata></createMetadata>`)
-
 	if err != nil {
 		slog.Error("err parsing", "error", err)
 		os.Exit(1)