Repository: amp-labs/connectors
Pull request #1311 (2025-01-29 16:21:15)

[CON-990] feat: Add ChiliPiper Write Connector
Tests:
![Screenshot 2025-01-30 at 11 55 42](https://github.com/user-attachments/assets/86a2b646-fb09-4d42-a8e7-c767e27fede6)

- Similar to Read Connector, I have added a map of objects to their paths, to support more objects.


Patch:
diff --git a/providers/chilipiper/test/distribution.json b/providers/chilipiper/test/distribution.json
new file mode 100644
index 00000000..e69de29b
diff --git a/providers/chilipiper/types.go b/providers/chilipiper/types.go
index deb17d3b..94e12e87 100644
--- a/providers/chilipiper/types.go
+++ b/providers/chilipiper/types.go
@@ -1,6 +1,8 @@
 package chilipiper
 
-import "github.com/amp-labs/connectors/common"
+import (
+	"github.com/amp-labs/connectors/internal/datautils"
+)
 
 const (
 	readpageSize     = "50"
@@ -13,22 +15,20 @@ const (
 )
 
 // objectPath maps an object to it's read path.
-var objectReadPath = map[string]string{ //nolint:gochecknoglobals
-	"workspace":       "workspace",
-	"team":            "team",
-	"distribution":    "distribution",
-	"workspace_users": "workspace/users",
-	// "meetings":        "meetings/meetings",
-	// "export_meetings": "meeting/meetings/export",
-}
-
-// supportsRead returns a unique path for reading the object.
-// or an error if the provided object is not supported.
-func supportsRead(object string) (string, error) {
-	path, ok := objectReadPath[object]
-	if !ok {
-		return "", common.ErrObjectNotSupported
-	}
+var supportedReadObjects = datautils.NewSet( //nolint:gochecknoglobals
+	"workspace",
+	"team",
+	"distribution",
+	"workspace/users",
+)
 
-	return path, nil
-}
+var supportedWriteObjects = datautils.NewSet( //nolint:gochecknoglobals
+	"distribution", // Allows updates only.
+	"user/invite",
+	"user/licenses",
+	"team/users/add",
+	"team/users/remove",
+	"workspace/users/add",
+	"workspace/users/remove",
+	"workspace/users/remove-from-all",
+)
diff --git a/providers/chilipiper/url.go b/providers/chilipiper/url.go
index 2972d0fb..2ce65c3b 100644
--- a/providers/chilipiper/url.go
+++ b/providers/chilipiper/url.go
@@ -1,18 +1,18 @@
 package chilipiper
 
 import (
+	"github.com/amp-labs/connectors/common"
 	"github.com/amp-labs/connectors/common/urlbuilder"
 )
 
 const restAPIVersionPrefix = "api/fire-edge/v1/org"
 
 func (conn *Connector) buildURL(objectName string, pageSize string) (string, error) {
-	path, err := supportsRead(objectName)
-	if err != nil {
-		return "", err
+	if !supportedReadObjects.Has(objectName) {
+		return "", common.ErrObjectNotSupported
 	}
 
-	url, err := urlbuilder.New(conn.BaseURL, restAPIVersionPrefix, path)
+	url, err := urlbuilder.New(conn.BaseURL, restAPIVersionPrefix, objectName)
 	if err != nil {
 		return "", err
 	}
@@ -21,3 +21,16 @@ func (conn *Connector) buildURL(objectName string, pageSize string) (string, err
 
 	return url.String(), nil
 }
+
+func (conn *Connector) buildWriteURL(object string) (*urlbuilder.URL, error) {
+	if !supportedWriteObjects.Has(object) {
+		return nil, common.ErrObjectNotSupported
+	}
+
+	writeURL, err := urlbuilder.New(conn.BaseURL, restAPIVersionPrefix, object)
+	if err != nil {
+		return nil, err
+	}
+
+	return writeURL, nil
+}
diff --git a/providers/chilipiper/write.go b/providers/chilipiper/write.go
new file mode 100644
index 00000000..7522cdcf
--- /dev/null
+++ b/providers/chilipiper/write.go
@@ -0,0 +1,68 @@
+package chilipiper
+
+import (
+	"context"
+
+	"github.com/amp-labs/connectors/common"
+)
+
+/*
+Sample Write Response
+{
+    "id": "4edf8761-e5ee-48b2-81c8-c5e4849481fc",
+    "workspaceId": "cad33722-df27-4691-bc11-1f2c89c1dd31",
+    "name": "Dev Team",
+    "members": [],
+    "metadata": {
+        "createdAt": "2025-01-24T09:37:47.321631Z",
+        "createdBy": "user/67929af0725ce43853fd2b8c",
+        "updatedAt": "2025-01-30T08:10:54.357774Z",
+        "updatedBy": "apikey/api_b830c0b6__",
+        "teamMembersMetadata": {
+            "addedAt": {}
+        },
+        "revision": 3
+    }
+}
+*/
+
+type response struct {
+	Id string `json:"id"`
+	// The rest changes depending on the action.
+}
+
+func (conn *Connector) Write(ctx context.Context, config common.WriteParams) (*common.WriteResult, error) {
+	if err := config.ValidateParams(); err != nil {
+		return nil, err
+	}
+
+	writeURL, err := conn.buildWriteURL(config.ObjectName)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(config.RecordId) > 0 {
+		writeURL.AddPath(config.RecordId)
+	}
+
+	resp, err := conn.Client.Post(ctx, writeURL.String(), config.RecordData)
+	if err != nil {
+		return nil, err
+	}
+
+	return constructWriteResponse(resp), nil
+}
+
+func constructWriteResponse(resp *common.JSONHTTPResponse) *common.WriteResult {
+	res, err := common.UnmarshalJSON[response](resp)
+	if err != nil {
+		return &common.WriteResult{
+			Success: true,
+		}
+	}
+
+	return &common.WriteResult{
+		RecordId: res.Id,
+		Success:  true,
+	}
+}


Test patch:
diff --git a/providers/chilipiper/test/add_user_team.json b/providers/chilipiper/test/add_user_team.json
new file mode 100644
index 00000000..912d33ad
--- /dev/null
+++ b/providers/chilipiper/test/add_user_team.json
@@ -0,0 +1,14 @@
+{
+  "id": "4edf8761-e5ee-48b2-81c8-c5e4849481fc",
+  "workspaceId": "cad33722-df27-4691-bc11-1f2c89c1dd31",
+  "name": "Dev Team",
+  "members": [],
+  "metadata": {
+    "createdAt": "2025-01-24T09:37:47.321631Z",
+    "createdBy": "user/67929af0725ce43853fd2b8c",
+    "updatedAt": "2025-01-30T08:10:54.357774Z",
+    "updatedBy": "apikey/api_b830c0b6__",
+    "teamMembersMetadata": { "addedAt": {} },
+    "revision": 3
+  }
+}
diff --git a/providers/chilipiper/write_test.go b/providers/chilipiper/write_test.go
new file mode 100644
index 00000000..15149115
--- /dev/null
+++ b/providers/chilipiper/write_test.go
@@ -0,0 +1,96 @@
+package chilipiper
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/amp-labs/connectors"
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/test/utils/mockutils/mockcond"
+	"github.com/amp-labs/connectors/test/utils/mockutils/mockserver"
+	"github.com/amp-labs/connectors/test/utils/testroutines"
+	"github.com/amp-labs/connectors/test/utils/testutils"
+)
+
+// nolint
+func TestWrite(t *testing.T) { // nolint:funlen,gocognit,cyclop
+	t.Parallel()
+
+	unsupportedResponse := testutils.DataFromFile(t, "unsupported.txt")
+	addUsers := testutils.DataFromFile(t, "add_user_team.json")
+	distribution := testutils.DataFromFile(t, "distribution.json")
+
+	tests := []testroutines.Write{
+		{
+			Name:         "Object Name is required",
+			Server:       mockserver.Dummy(),
+			ExpectedErrs: []error{common.ErrMissingObjects},
+		},
+		{
+			Name:         "RecordData is required",
+			Input:        common.WriteParams{ObjectName: "leads"},
+			Server:       mockserver.Dummy(),
+			ExpectedErrs: []error{common.ErrMissingRecordData},
+		},
+
+		{
+			Name:  "Unsupported object",
+			Input: common.WriteParams{ObjectName: "arsenal", RecordData: "dummy"},
+			Server: mockserver.Conditional{
+				Setup: mockserver.ContentJSON(),
+				If:    mockcond.MethodPOST(),
+				Then:  mockserver.Response(http.StatusNotFound, unsupportedResponse),
+			}.Server(),
+			ExpectedErrs: []error{common.ErrObjectNotSupported},
+		},
+		{
+			Name: "Successfull add users in a team",
+			Input: common.WriteParams{ObjectName: "team/users/add", RecordData: map[string]any{
+				"teamId": "4edf8761-e5ee-48b2-81c8-c5e4849481fc",
+				"userIds": []string{
+					"67929af0725ce43853fd2b8c",
+				},
+			}},
+			Server: mockserver.Conditional{
+				Setup: mockserver.ContentJSON(),
+				If:    mockcond.MethodPOST(),
+				Then:  mockserver.Response(http.StatusOK, addUsers),
+			}.Server(),
+			Expected: &common.WriteResult{
+				Success:  true,
+				RecordId: "4edf8761-e5ee-48b2-81c8-c5e4849481fc",
+			},
+			ExpectedErrs: nil,
+		},
+		{
+			Name: "Successfully update a distribution",
+			Input: common.WriteParams{
+				ObjectName: "distribution",
+				RecordId:   "66d573f1bb530101b230db6f",
+				RecordData: map[string]any{
+					"resetDistribution": true,
+				},
+			},
+			Server: mockserver.Conditional{
+				Setup: mockserver.ContentJSON(),
+				If:    mockcond.MethodPOST(),
+				Then:  mockserver.Response(http.StatusOK, distribution),
+			}.Server(),
+			Expected: &common.WriteResult{
+				Success: true,
+			},
+			ExpectedErrs: nil,
+		},
+	}
+
+	for _, tt := range tests {
+		// nolint:varnamelen
+		t.Run(tt.Name, func(t *testing.T) {
+			t.Parallel()
+
+			tt.Run(t, func() (connectors.WriteConnector, error) {
+				return constructTestConnector(tt.Server.URL)
+			})
+		})
+	}
+}
diff --git a/test/chilipiper/write/write.go b/test/chilipiper/write/write.go
new file mode 100644
index 00000000..b880dede
--- /dev/null
+++ b/test/chilipiper/write/write.go
@@ -0,0 +1,112 @@
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"log/slog"
+	"os"
+
+	"github.com/amp-labs/connectors/common"
+	ch "github.com/amp-labs/connectors/providers/chilipiper"
+	"github.com/amp-labs/connectors/test/chilipiper"
+)
+
+func main() {
+	os.Exit(MainFn())
+}
+
+func MainFn() int {
+	ctx := context.Background()
+	conn := chilipiper.GetChiliPiperConnector(ctx)
+
+	if err := removeUsersTeam(ctx, conn); err != nil {
+		slog.Error(err.Error())
+	}
+
+	if err := updateDistribution(ctx, conn); err != nil {
+		slog.Error(err.Error())
+	}
+
+	if err := inviteUsers(ctx, conn); err != nil {
+		slog.Error(err.Error())
+	}
+
+	return 0
+}
+
+func removeUsersTeam(ctx context.Context, conn *ch.Connector) error {
+	prms := common.WriteParams{
+		ObjectName: "team/users/remove",
+		RecordData: map[string]any{
+			"teamId": "4edf8761-e5ee-48b2-81c8-c5e4849481fc",
+			"userIds": []string{
+				"67929af0725ce43853fd2b8c",
+			},
+		},
+	}
+
+	result, err := conn.Write(ctx, prms)
+	if err != nil {
+		return err
+	}
+
+	// Print the results
+	jsonStr, err := json.MarshalIndent(result, "", "  ")
+	if err != nil {
+		return err
+	}
+
+	fmt.Println(string(jsonStr))
+
+	return nil
+}
+
+func updateDistribution(ctx context.Context, conn *ch.Connector) error {
+	prms := common.WriteParams{
+		ObjectName: "distribution",
+		RecordId:   "3b3baa8f-ee2c-40ed-92a0-a08aae954925",
+		RecordData: map[string]any{
+			"resetDistribution": false,
+		},
+	}
+
+	result, err := conn.Write(ctx, prms)
+	if err != nil {
+		return err
+	}
+
+	// Print the results
+	jsonStr, err := json.MarshalIndent(result, "", "  ")
+	if err != nil {
+		return err
+	}
+
+	fmt.Println(string(jsonStr))
+
+	return nil
+}
+
+func inviteUsers(ctx context.Context, conn *ch.Connector) error {
+	prms := common.WriteParams{
+		ObjectName: "user/invite",
+		RecordData: map[string]any{
+			"email": "pepkarage@gmail.com",
+		},
+	}
+
+	result, err := conn.Write(ctx, prms)
+	if err != nil {
+		return err
+	}
+
+	// Print the results
+	jsonStr, err := json.MarshalIndent(result, "", "  ")
+	if err != nil {
+		return err
+	}
+
+	fmt.Println(string(jsonStr))
+
+	return nil
+}