Repository: amp-labs/connectors
Pull request #1848 (2025-07-08 11:46:45)

[CON-1985] feat(fathom): add write
## Checklist
- [x] Connector uses `internal/components`
- [ ] Metadata uses V2 metadata format
- [ ] Read supports pagination
- [x] Raw response is returned as is, no formatting or flattening is performed.
- [x] Write payloads should accept what `ReadResults.Fields` is returning. Any unnecessary nesting around the input is removed.
- [ ] Provider errors are mapped if non-standard (errors with 200 response code are converted to 4XX)
- [x] Custom fields, if not human readable names, are resolved to readable names.
- [x] Unit tests cover read/write/metadata logic (placed in /tests/<provider>)
- [x] Appropriate object names are used. Objects need to be resources, not actions (`jobs` and not `jobs.list`).
- [ ] Modules are only being added because:
  - [ ] The same base URL cannot be used to make a proxy call to objects in all modules
  - [ ] Different base URLs (drive.google.com vs google.com)
  - [ ] Object name collisions (same object name exists in two or more modules)


## Sample Read Response
<img width="700" alt="image" src="https://github.com/user-attachments/assets/6963f248-491b-4f38-b4db-0937baa8f3e2" />



## Test cases Result
<img width="1397" alt="image" src="https://github.com/user-attachments/assets/b33748ed-0b53-4b10-91c3-0e5e43dec132" />





Patch:
diff --git a/providers/fathom/connector.go b/providers/fathom/connector.go
index 61a64eac..c2cdfbb8 100644
--- a/providers/fathom/connector.go
+++ b/providers/fathom/connector.go
@@ -6,6 +6,7 @@ import (
 	"github.com/amp-labs/connectors/internal/components/operations"
 	"github.com/amp-labs/connectors/internal/components/reader"
 	"github.com/amp-labs/connectors/internal/components/schema"
+	"github.com/amp-labs/connectors/internal/components/writer"
 	"github.com/amp-labs/connectors/providers"
 )
 
@@ -19,6 +20,7 @@ type Connector struct {
 	// Supported operations
 	components.SchemaProvider
 	components.Reader
+	components.Writer
 }
 
 func NewConnector(params common.ConnectorParams) (*Connector, error) {
@@ -55,5 +57,16 @@ func constructor(base *components.Connector) (*Connector, error) {
 		},
 	)
 
+	connector.Writer = writer.NewHTTPWriter(
+		connector.HTTPClient().Client,
+		registry,
+		connector.ProviderContext.Module(),
+		operations.WriteHandlers{
+			BuildRequest:  connector.buildWriteRequest,
+			ParseResponse: connector.parseWriteResponse,
+			ErrorHandler:  common.InterpretError,
+		},
+	)
+
 	return connector, nil
 }
diff --git a/providers/fathom/handlers.go b/providers/fathom/handlers.go
index cfddc80c..4d37521e 100644
--- a/providers/fathom/handlers.go
+++ b/providers/fathom/handlers.go
@@ -1,7 +1,9 @@
 package fathom
 
 import (
+	"bytes"
 	"context"
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"time"
@@ -9,6 +11,7 @@ import (
 	"github.com/amp-labs/connectors/common"
 	"github.com/amp-labs/connectors/common/naming"
 	"github.com/amp-labs/connectors/common/urlbuilder"
+	"github.com/amp-labs/connectors/internal/jsonquery"
 )
 
 const (
@@ -113,6 +116,57 @@ func (c *Connector) parseReadResponse(
 	)
 }
 
+func (c *Connector) buildWriteRequest(ctx context.Context, params common.WriteParams) (*http.Request, error) {
+	method := http.MethodPost
+
+	if params.RecordId != "" {
+		method = http.MethodPut
+	}
+
+	url, err := urlbuilder.New(c.ProviderInfo().BaseURL, restAPIPrefix, apiVersion, params.ObjectName)
+	if err != nil {
+		return nil, err
+	}
+
+	jsonData, err := json.Marshal(params.RecordData)
+	if err != nil {
+		return nil, err
+	}
+
+	return http.NewRequestWithContext(ctx, method, url.String(), bytes.NewReader(jsonData))
+}
+
+func (c *Connector) parseWriteResponse(
+	ctx context.Context,
+	params common.WriteParams,
+	request *http.Request,
+	response *common.JSONHTTPResponse,
+) (*common.WriteResult, error) {
+	body, ok := response.Body()
+	if !ok {
+		return &common.WriteResult{
+			Success: true,
+		}, nil
+	}
+
+	recordID, err := jsonquery.New(body).StrWithDefault("id", "")
+	if err != nil {
+		return nil, err
+	}
+
+	resp, err := jsonquery.Convertor.ObjectToMap(body)
+	if err != nil {
+		return nil, err
+	}
+
+	return &common.WriteResult{
+		Success:  true,
+		RecordId: recordID,
+		Errors:   nil,
+		Data:     resp,
+	}, nil
+}
+
 func inferValueTypeFromData(value any) common.ValueType {
 	if value == nil {
 		return common.ValueTypeOther
diff --git a/providers/fathom/support.go b/providers/fathom/support.go
index d50d9944..3ca25370 100644
--- a/providers/fathom/support.go
+++ b/providers/fathom/support.go
@@ -12,6 +12,9 @@ func supportedOperations() components.EndpointRegistryInput {
 	readSupport := []string{
 		"meetings", "teams", "team_members",
 	}
+	writeSupport := []string{
+		"webhooks",
+	}
 
 	return components.EndpointRegistryInput{
 		common.ModuleRoot: {
@@ -19,6 +22,10 @@ func supportedOperations() components.EndpointRegistryInput {
 				Endpoint: fmt.Sprintf("{%s}", strings.Join(readSupport, ",")),
 				Support:  components.ReadSupport,
 			},
+			{
+				Endpoint: fmt.Sprintf("{%s}", strings.Join(writeSupport, ",")),
+				Support:  components.WriteSupport,
+			},
 		},
 	}
 }


Test patch:
diff --git a/providers/fathom/test/create-webhooks.json b/providers/fathom/test/create-webhooks.json
new file mode 100644
index 00000000..e0523c3d
--- /dev/null
+++ b/providers/fathom/test/create-webhooks.json
@@ -0,0 +1,10 @@
+{
+  "id": "ikEoQ4bVoq4JYUmc",
+  "url": "https://example.com/webhook",
+  "secret": "whsec_x6EV6NIAAz3ldclszNJTwrow",
+  "created_at": "2025-06-30T10:40:46Z",
+  "include_transcript": true,
+  "include_crm_matches": true,
+  "include_summary": true,
+  "include_action_items": true
+}
\ No newline at end of file
diff --git a/providers/fathom/write_test.go b/providers/fathom/write_test.go
new file mode 100644
index 00000000..d4a71a49
--- /dev/null
+++ b/providers/fathom/write_test.go
@@ -0,0 +1,72 @@
+package fathom
+
+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"
+)
+
+func TestWrite(t *testing.T) { // nolint:funlen,gocognit,cyclop
+	t.Parallel()
+
+	createWebhooksResponse := testutils.DataFromFile(t, "create-webhooks.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: "Successfully creation of a webhook",
+			Input: common.WriteParams{ObjectName: "webhooks", RecordData: map[string]any{
+				"destination_url":      "https://play.svix.com/in/e_5U95s0OihUbc32B8UDA1MoAaAG2/",
+				"include_action_items": true,
+			}},
+			Server: mockserver.Conditional{
+				Setup: mockserver.ContentJSON(),
+				If:    mockcond.MethodPOST(),
+				Then:  mockserver.Response(http.StatusOK, createWebhooksResponse),
+			}.Server(),
+			Expected: &common.WriteResult{
+				Success:  true,
+				RecordId: "ikEoQ4bVoq4JYUmc",
+				Data: map[string]any{
+					"id":                   "ikEoQ4bVoq4JYUmc",
+					"url":                  "https://example.com/webhook",
+					"secret":               "whsec_x6EV6NIAAz3ldclszNJTwrow",
+					"created_at":           "2025-06-30T10:40:46Z",
+					"include_transcript":   true,
+					"include_crm_matches":  true,
+					"include_summary":      true,
+					"include_action_items": 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/fathom/write/main.go b/test/fathom/write/main.go
new file mode 100644
index 00000000..0793a9d7
--- /dev/null
+++ b/test/fathom/write/main.go
@@ -0,0 +1,60 @@
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"log/slog"
+	"os"
+
+	"github.com/amp-labs/connectors/common"
+	cc "github.com/amp-labs/connectors/providers/fathom"
+	"github.com/amp-labs/connectors/test/fathom"
+)
+
+func main() {
+	if err := run(); err != nil {
+		slog.Error(err.Error())
+	}
+}
+
+func run() error {
+	ctx := context.Background()
+
+	conn := fathom.GetFathomConnector(ctx)
+
+	_, err := testCreatingWebhooks(ctx, conn)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func testCreatingWebhooks(ctx context.Context, conn *cc.Connector) (string, error) {
+	params := common.WriteParams{
+		ObjectName: "webhooks",
+		RecordData: map[string]any{
+			"destination_url":      "https://play.svix.com/in/e_5U95s0OihUbc32B8UDA1MoAaAG2/",
+			"include_action_items": true,
+		},
+	}
+
+	slog.Info("Creating webhooks...")
+
+	res, err := conn.Write(ctx, params)
+	if err != nil {
+		return "", err
+	}
+
+	// Print the results
+	jsonStr, err := json.MarshalIndent(res, "", "  ")
+	if err != nil {
+		return "", fmt.Errorf("error marshalling JSON: %w", err)
+	}
+
+	_, _ = os.Stdout.Write(jsonStr)
+	_, _ = os.Stdout.WriteString("\n")
+
+	return res.Data["id"].(string), nil
+}