Repository: amp-labs/connectors
Pull request #1412 (2025-02-27 07:55:30)

[CON-1255] feat: Enable proxy: Instantly(v2)
## Checklist
- [x] Ran Linter
- [x] Created PR from non-main branch

## Notes
No fishy behaviour was observed. All the requests/responses were parsed correctly.
It supports batching and pagination.

## Testing
### GET ALL
URL: https://proxy.withampersand.com/v2/campaigns
![image](https://github.com/user-attachments/assets/f65fc471-71ac-420a-8cfc-d0364b05d161)

### GET
URL: https://proxy.withampersand.com/v2/campaigns/85dbcb5e-03e9-4824-84a8-f2037d544436
![image](https://github.com/user-attachments/assets/f2fd24af-cbee-43cb-8f54-3de52e8e54af)

### POST
URL: https://proxy.withampersand.com/v2/campaigns
![image](https://github.com/user-attachments/assets/ed054f9a-42ac-441e-bcd8-da5dc0718fdb)

### PATCH
URL: https://proxy.withampersand.com/v2/campaigns/85dbcb5e-03e9-4824-84a8-f2037d544436
![image](https://github.com/user-attachments/assets/c521319a-1c3e-49e0-bc0d-f670d2def7f8)

### DELETE
URL: https://proxy.withampersand.com/v2/campaigns/0d68b52e-8d35-4a22-88b2-74b1c3d0552d
![image](https://github.com/user-attachments/assets/a07e570f-5054-4b6f-8eb3-bb106de22efc)

### Pagination
Requesting a paginated list of campaigns using **limit**:
URL : https://proxy.withampersand.com/v2/campaigns?limit=4
![image](https://github.com/user-attachments/assets/586f1530-39f9-4b1e-9aba-da463e29dfc7)

Requesting the next paginated list of campaigns using the **limit** and **starting_after** parameters, where the value of **starting_after** is taken from the **next_starting_after** field in the previous paginated response:
URL : https://proxy.withampersand.com/v2/campaigns?limit=4&starting_after=65d890fe-ae4d-43d0-b014-af0e89b6281f
![image](https://github.com/user-attachments/assets/4b9b3b92-059d-44b0-8d60-2d3a5350ee14)

## Invalid request
### Invalid endpoint
![image](https://github.com/user-attachments/assets/0464d186-ef8e-4151-8198-ab314f2951cc)

### Invalid method
![image](https://github.com/user-attachments/assets/c5e8a559-c54b-44b6-89ba-f757417d5281)

## Success Console Operation
![image](https://github.com/user-attachments/assets/d7ba45eb-85f9-4c7c-9d61-4e81be133ac2)
![image](https://github.com/user-attachments/assets/fe3bdca5-0c1b-4a5f-b365-9ce34c8dd786)

## Failure Console Operation
![image](https://github.com/user-attachments/assets/0dee74dc-266b-4a69-b177-3a827b700729)




Patch:
diff --git a/common/types.go b/common/types.go
index 5a9d5b6a..b71ee540 100644
--- a/common/types.go
+++ b/common/types.go
@@ -500,11 +500,12 @@ type SubscribeConnector interface {
 		ctx context.Context,
 		params SubscriptionRegistrationParams,
 	) (*RegistrationResult, error)
-	UpdateRegistration(
-		ctx context.Context,
-		params SubscriptionRegistrationParams,
-		previousResult RegistrationResult,
-	) (*RegistrationResult, error)
+	// TODO: Uncomment when we implement UpdateRegistration in Salesforce
+	// UpdateRegistration(
+	// 	ctx context.Context,
+	// 	params SubscriptionRegistrationParams,
+	// 	previousResult RegistrationResult,
+	// ) (*RegistrationResult, error)
 	DeleteRegistration(
 		ctx context.Context,
 		previousResult RegistrationResult,
@@ -514,11 +515,12 @@ type SubscribeConnector interface {
 		ctx context.Context,
 		params SubscribeParams,
 	) (*SubscriptionResult, error)
-	UpdateSubscription(
-		ctx context.Context,
-		params SubscribeParams,
-		previousResult SubscriptionResult,
-	) (*SubscriptionResult, error)
+	// TODO: Uncomment when we implement UpdateSubscription in Salesforce
+	// UpdateSubscription(
+	// 	ctx context.Context,
+	// 	params SubscribeParams,
+	// 	previousResult SubscriptionResult,
+	// ) (*SubscriptionResult, error)
 	DeleteSubscription(
 		ctx context.Context,
 		previousResult SubscriptionResult,
diff --git a/go.mod b/go.mod
index 80dccfc5..2488b5fe 100644
--- a/go.mod
+++ b/go.mod
@@ -20,7 +20,7 @@ require (
 	github.com/stretchr/testify v1.10.0
 	gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a
 	golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329
-	golang.org/x/oauth2 v0.26.0
+	golang.org/x/oauth2 v0.27.0
 	golang.org/x/text v0.22.0
 )
 
diff --git a/go.sum b/go.sum
index 52fa5317..6bfeeb07 100644
--- a/go.sum
+++ b/go.sum
@@ -143,8 +143,8 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
 golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
 golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
 golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
-golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
-golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
+golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
diff --git a/providers/gorgias/connector.go b/providers/gorgias/connector.go
new file mode 100644
index 00000000..2128d911
--- /dev/null
+++ b/providers/gorgias/connector.go
@@ -0,0 +1,45 @@
+package gorgias
+
+import (
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/internal/components"
+	"github.com/amp-labs/connectors/internal/components/operations"
+	"github.com/amp-labs/connectors/internal/components/schema"
+	"github.com/amp-labs/connectors/providers"
+)
+
+const restAPIPrefix = "api"
+
+type Connector struct {
+	// Basic connector
+	*components.Connector
+
+	// Require authenticated client
+	common.RequireAuthenticatedClient
+	// Require workspace
+	common.RequireWorkspace
+
+	// Supported operations
+	components.SchemaProvider
+}
+
+func NewConnector(params common.Parameters) (*Connector, error) {
+	// Create base connector with provider info
+	return components.Initialize(providers.Gorgias, params, constructor)
+}
+
+func constructor(base *components.Connector) (*Connector, error) {
+	connector := &Connector{Connector: base}
+
+	// Set the metadata provider for the connector
+	connector.SchemaProvider = schema.NewObjectSchemaProvider(
+		connector.HTTPClient().Client,
+		schema.FetchModeParallel,
+		operations.SingleObjectMetadataHandlers{
+			BuildRequest:  connector.buildSingleObjectMetadataRequest,
+			ParseResponse: connector.parseSingleObjectMetadataResponse,
+		},
+	)
+
+	return connector, nil
+}
diff --git a/providers/gorgias/handlers.go b/providers/gorgias/handlers.go
new file mode 100644
index 00000000..63242757
--- /dev/null
+++ b/providers/gorgias/handlers.go
@@ -0,0 +1,73 @@
+package gorgias
+
+import (
+	"context"
+	"net/http"
+
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/common/naming"
+	"github.com/amp-labs/connectors/common/urlbuilder"
+)
+
+const (
+	account          = "account"
+	limitQuery       = "limit"
+	metadataPageSize = "1"
+)
+
+type dataResponse struct {
+	Data []map[string]any `json:"data"`
+	// Request Metadata Fields
+}
+
+func (c *Connector) buildSingleObjectMetadataRequest(ctx context.Context, objectName string) (*http.Request, error) {
+	url, err := urlbuilder.New(c.ProviderInfo().BaseURL, restAPIPrefix, objectName)
+	if err != nil {
+		return nil, err
+	}
+
+	// Limit response to 1 record data.
+	url.WithQueryParam(limitQuery, metadataPageSize)
+
+	return http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
+}
+
+func (c *Connector) parseSingleObjectMetadataResponse(
+	ctx context.Context,
+	objectName string,
+	response *common.JSONHTTPResponse,
+) (*common.ObjectMetadata, error) {
+	objectMetadata := common.ObjectMetadata{
+		FieldsMap:   make(map[string]string),
+		DisplayName: naming.CapitalizeFirstLetterEveryWord(objectName),
+	}
+
+	// All supported objects return a response following the `dataResponse` schema,
+	// with the exception of the `account` object.
+	switch objectName {
+	case account:
+		record, err := common.UnmarshalJSON[map[string]any](response)
+		if err != nil {
+			return nil, common.ErrFailedToUnmarshalBody
+		}
+
+		for fld := range *record {
+			objectMetadata.FieldsMap[fld] = fld
+		}
+	default:
+		records, err := common.UnmarshalJSON[dataResponse](response)
+		if err != nil {
+			return nil, common.ErrFailedToUnmarshalBody
+		}
+
+		if len(records.Data) == 0 {
+			return nil, common.ErrMissingExpectedValues
+		}
+
+		for fld := range records.Data[0] {
+			objectMetadata.FieldsMap[fld] = fld
+		}
+	}
+
+	return &objectMetadata, nil
+}
diff --git a/providers/instantly.go b/providers/instantly.go
index 22142ca3..11373dc8 100644
--- a/providers/instantly.go
+++ b/providers/instantly.go
@@ -73,7 +73,7 @@ func init() {
 				Upsert: false,
 				Delete: false,
 			},
-			Proxy:     false,
+			Proxy:     true,
 			Read:      false,
 			Subscribe: false,
 			Write:     false,
diff --git a/providers/salesforce/register.go b/providers/salesforce/register.go
index 2c9803d7..a4d6de1b 100644
--- a/providers/salesforce/register.go
+++ b/providers/salesforce/register.go
@@ -139,15 +139,7 @@ func (c *Connector) register(
 
 // DeleteRegistration will delete the Salesforce objects created during registration.
 // It will delete the EventRelayConfig, NamedCredential, and EventChannel.
-func (c *Connector) DeleteRegistration(ctx context.Context, registration *common.RegistrationResult) error {
-	if registration == nil {
-		return fmt.Errorf("%w: registration is null", errMissingParams)
-	}
-
-	if registration.Result == nil {
-		return fmt.Errorf("%w: registration result is null", errMissingParams)
-	}
-
+func (c *Connector) DeleteRegistration(ctx context.Context, registration common.RegistrationResult) error {
 	validate := validator.New()
 	if err := validate.Struct(registration.Result); err != nil {
 		return fmt.Errorf("invalid registration result: %w", err)


Test patch:
diff --git a/test/gorgias/connector.go b/test/gorgias/connector.go
new file mode 100644
index 00000000..d64bcf1d
--- /dev/null
+++ b/test/gorgias/connector.go
@@ -0,0 +1,55 @@
+package gorgias
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/common/scanning/credscanning"
+	"github.com/amp-labs/connectors/providers"
+	"github.com/amp-labs/connectors/providers/gorgias"
+	"github.com/amp-labs/connectors/test/utils"
+	"golang.org/x/oauth2"
+)
+
+func GetConnector(ctx context.Context) *gorgias.Connector {
+	filePath := credscanning.LoadPath(providers.Gorgias)
+	reader := utils.MustCreateProvCredJSON(filePath, true, true)
+
+	client, err := common.NewOAuthHTTPClient(ctx,
+		common.WithOAuthClient(http.DefaultClient),
+		common.WithOAuthConfig(getConfig(reader)),
+		common.WithOAuthToken(reader.GetOauthToken()),
+	)
+	if err != nil {
+		utils.Fail(err.Error())
+	}
+
+	conn, err := gorgias.NewConnector(common.Parameters{
+		AuthenticatedClient: client,
+		Workspace:           "ampersand",
+	})
+	if err != nil {
+		utils.Fail("error creating connector", "error", err)
+	}
+
+	return conn
+}
+
+func getConfig(reader *credscanning.ProviderCredentials) *oauth2.Config {
+	cfg := oauth2.Config{
+		ClientID:     reader.Get(credscanning.Fields.ClientId),
+		ClientSecret: reader.Get(credscanning.Fields.ClientSecret),
+		Endpoint: oauth2.Endpoint{
+			AuthURL:   fmt.Sprintf("https://%s.gorgias.com/oauth/authorize", "ampersand"),
+			TokenURL:  fmt.Sprintf("https://%s.gorgias.com/oauth/token", "ampersand"),
+			AuthStyle: oauth2.AuthStyleAutoDetect,
+		},
+		Scopes: []string{
+			"offline", "write:all",
+		},
+	}
+
+	return &cfg
+}
diff --git a/test/gorgias/metadata/main.go b/test/gorgias/metadata/main.go
new file mode 100644
index 00000000..b9240d8c
--- /dev/null
+++ b/test/gorgias/metadata/main.go
@@ -0,0 +1,31 @@
+package main
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/amp-labs/connectors/test/gorgias"
+	"github.com/amp-labs/connectors/test/utils"
+)
+
+func main() {
+	if err := run(); err != nil {
+		utils.Fail(err.Error())
+	}
+}
+
+func run() error {
+	ctx := context.Background()
+	connector := gorgias.GetConnector(ctx)
+
+	m, err := connector.ListObjectMetadata(ctx, []string{"account", "customers", "events"})
+	if err != nil {
+		return err
+	}
+
+	// Print the results
+	fmt.Println("Results: ", m.Result)
+	fmt.Println("Errors: ", m.Errors)
+
+	return nil
+}
diff --git a/test/salesforce/subscribe/register.go b/test/salesforce/subscribe/register.go
index cc55bb92..4656c5bb 100644
--- a/test/salesforce/subscribe/register.go
+++ b/test/salesforce/subscribe/register.go
@@ -79,7 +79,7 @@ func main() {
 
 	fmt.Println("Delete subscription successful")
 
-	if err := conn.DeleteRegistration(ctx, result); err != nil {
+	if err := conn.DeleteRegistration(ctx, *result); err != nil {
 		logging.Logger(ctx).Error("Error rolling back registration", "error", err)
 
 		return