Repository: amp-labs/connectors
Pull request #1513 (2025-03-21 14:01:51)

[CON-1610] feat(attio): write: To support standard & custom objects
## Sample Write Response

### For Lists object(POST)
![image](https://github.com/user-attachments/assets/279ec3cc-c194-489d-ac35-3f431bc7409b)

### For Lists object(PATCH)
![image](https://github.com/user-attachments/assets/8b5469ba-2906-46ba-8cfc-0b4b56f10744)

### For Companies object(POST)
![image](https://github.com/user-attachments/assets/9a5d1d10-9ed1-4b42-8e84-569712b8acad)

### For Companies Object(PUT)
![image](https://github.com/user-attachments/assets/6654870a-ac76-46ed-ba1a-f180550238be)

## Test case Result
![image](https://github.com/user-attachments/assets/f0304151-cf27-4233-b4e4-5e86bb390b36)






Patch:
diff --git a/providers/attio/README.md b/providers/attio/README.md
index d4ef9399..92467048 100644
--- a/providers/attio/README.md
+++ b/providers/attio/README.md
@@ -12,11 +12,11 @@ Attio API version : v2
 | Workspace members | workspace_members | read
 | Tasks  | tasks | read and write
 | Notes  | notes | read and write
-| People | people| read
-| Companies | companies | read
-| Users | users | read
-| Deals | deals | read
-| Workspaces | workspaces | read
+| People | people| read and write
+| Companies | companies | read and write
+| Users | users | read and write
+| Deals | deals | read and write
+| Workspaces | workspaces | read and write
 
 The objects mentioned above, such as People, Companies, Users, Deals, and Workspaces, are Attio standard objects. The remaining objects, like Lists, Workspace members, Tasks, and Notes, are Attio non-standard objects.
 
@@ -35,3 +35,10 @@ For non-standard objects, directly used the object's endpoint.
 For standard/custom objects, a separate read function "readStandardOrCustomObject" is used, which calls the endpoint to get the response: https://developers.attio.com/reference/post_v2-objects-object-records-query
 
 For non-standard object, a separate read function "readGeneralAPI" is used and directly used the object's endpoint to get the response.
+
+## Write Functions
+For non-standard objects, introduce a variable "supportWriteObjects" and use the object's endpoint directly to get the response
+
+For standard/custom objects,
+Used endpoint to create: https://developers.attio.com/reference/post_v2-objects-object-records
+Used endpoint to update: https://developers.attio.com/reference/put_v2-objects-object-records-record-id
diff --git a/providers/attio/objectNames.go b/providers/attio/objectNames.go
index 6446ed0f..e9cd2697 100644
--- a/providers/attio/objectNames.go
+++ b/providers/attio/objectNames.go
@@ -9,7 +9,9 @@ const (
 	objectNameNotes            = "notes"
 )
 
-var supportedObjectsByWrite = datautils.NewSet( //nolint:gochecknoglobals
+// supportWriteObjects represents the APIs listed under the Attio API section in the docs
+// (this does not cover the entire Attio API). Reference: https://developers.attio.com/reference.
+var supportWriteObjects = datautils.NewSet( //nolint:gochecknoglobals
 	objectNameLists,
 	objectNameTasks,
 	objectNameNotes,
diff --git a/providers/attio/url.go b/providers/attio/url.go
index be43e2f8..5e894a33 100644
--- a/providers/attio/url.go
+++ b/providers/attio/url.go
@@ -25,3 +25,8 @@ func (c *Connector) getOptionsURL(objName, attributeID string) (*urlbuilder.URL,
 func (c *Connector) getObjectReadURL(objName string) (*urlbuilder.URL, error) {
 	return urlbuilder.New(c.BaseURL, apiVersion, "objects", objName, "records", "query")
 }
+
+// Relative URL for retrieving standarad and custom object write URL.
+func (c *Connector) getObjectWriteURL(objName string) (*urlbuilder.URL, error) {
+	return urlbuilder.New(c.BaseURL, apiVersion, "objects", objName, "records")
+}
diff --git a/providers/attio/write.go b/providers/attio/write.go
index cf6448a6..2094f804 100644
--- a/providers/attio/write.go
+++ b/providers/attio/write.go
@@ -1,4 +1,3 @@
-// nolint
 package attio
 
 import (
@@ -7,6 +6,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"
 	"github.com/spyzhov/ajson"
 )
@@ -19,11 +19,7 @@ func (c *Connector) Write(ctx context.Context, config common.WriteParams) (*comm
 		return nil, err
 	}
 
-	if !supportedObjectsByWrite.Has(config.ObjectName) {
-		return nil, common.ErrOperationNotSupportedForObject
-	}
-
-	url, err := c.getApiURL(config.ObjectName)
+	url, err := c.buildWriteURL(config)
 	if err != nil {
 		return nil, err
 	}
@@ -34,7 +30,11 @@ func (c *Connector) Write(ctx context.Context, config common.WriteParams) (*comm
 		write = c.Client.Post
 	} else {
 		// updating resource by patch method.
-		write = c.Client.Patch
+		write = c.Client.Put
+
+		if supportWriteObjects.Has(config.ObjectName) {
+			write = c.Client.Patch
+		}
 
 		url.AddPath(config.RecordId)
 	}
@@ -55,8 +55,29 @@ func (c *Connector) Write(ctx context.Context, config common.WriteParams) (*comm
 	return constructWriteResult(config.ObjectName, body)
 }
 
+func (c *Connector) buildWriteURL(config common.WriteParams) (*urlbuilder.URL, error) {
+	// supportWriteObjects represents the APIs listed under the Attio API section in the docs
+	// (this does not cover the entire Attio API). Reference: https://developers.attio.com/reference.
+	if supportWriteObjects.Has(config.ObjectName) {
+		return c.getApiURL(config.ObjectName)
+	}
+
+	url, err := c.getObjectWriteURL(config.ObjectName)
+	if err != nil {
+		return nil, err
+	}
+
+	return url, nil
+}
+
 func constructWriteResult(objName string, body *ajson.Node) (*common.WriteResult, error) {
-	obj := naming.NewSingularString(objName)
+	var obj naming.SingularString
+
+	if supportWriteObjects.Has(objName) {
+		obj = naming.NewSingularString(objName)
+	} else {
+		obj = naming.NewSingularString("record")
+	}
 
 	objectResponse, err := jsonquery.New(body).ObjectRequired("data")
 	if err != nil {


Test patch:
diff --git a/providers/attio/test/write_companies_record.json b/providers/attio/test/write_companies_record.json
new file mode 100644
index 00000000..dab752de
--- /dev/null
+++ b/providers/attio/test/write_companies_record.json
@@ -0,0 +1,23 @@
+{
+    "data": {
+        "id": {
+            "workspace_id": "63d34516-b287-4c27-9d28-fe2adbebcd50",
+            "object_id": "1fa986a6-952e-4e92-ba01-acca61a7b616",
+            "record_id": "238747c9-6dce-4475-8928-fe66ddfe8526"
+        },
+        "values": {
+            "name": [
+                {
+                    "active_from": "2025-03-21T10:27:09.906000000Z",
+                    "active_until": null,
+                    "created_by_actor": {
+                        "type": "api-token",
+                        "id": "0d91718e-9193-47e7-8b6c-6feeae40dcc1"
+                    },
+                    "value": "pay",
+                    "attribute_type": "text"
+                }
+            ]
+        }
+    }
+}
\ No newline at end of file
diff --git a/providers/attio/test/write_lists.json b/providers/attio/test/write_lists.json
index e8f5b8d7..31457305 100644
--- a/providers/attio/test/write_lists.json
+++ b/providers/attio/test/write_lists.json
@@ -1,25 +1,10 @@
 {
 "data": {
     "api_slug": "sales_investing",
-    "created_at": "2024-10-04T10:03:02.070000000Z",
-    "created_by_actor": {
-     "id": "00bcd19c-9a89-467f-bf61-4e1a7f8b8754",
-     "type": "api-token"
-    },
     "id": {
      "list_id": "e09a041c-0555-4bb2-8f6e-997bfc9b54e8",
      "workspace_id": "0d4d7fa2-d6e8-4a61-a7dc-e178405ff3c6"        
     },
-    "name": "Sales",
-    "parent_object": [
-     "companies"
-    ],
-    "workspace_access": "full-access",
-    "workspace_member_access": [
-     {
-      "level": "full-access",
-      "workspace_member_id": "67af46e4-a450-4fee-a1d1-39729b3af771"
-     }
-    ]
+    "name": "Sales"
    }
 }
\ No newline at end of file
diff --git a/providers/attio/test/write_notes.json b/providers/attio/test/write_notes.json
index 514a8bf0..80e20010 100644
--- a/providers/attio/test/write_notes.json
+++ b/providers/attio/test/write_notes.json
@@ -1,17 +1,10 @@
 {
     "data": {
     "content_plaintext": "summary",
-    "created_at": "2024-10-04T10:03:03.347000000Z",
-    "created_by_actor": {
-     "id": "00bcd19c-9a89-467f-bf61-4e1a7f8b8754",
-     "type": "api-token"
-    },
     "id": {
      "note_id": "126e58a5-5e3f-4644-89ff-6474e97fcecd",
      "workspace_id": "0d4d7fa2-d6e8-4a61-a7dc-e178405ff3c6"
     },
-    "parent_object": "companies",
-    "parent_record_id": "ec902ed9-aab7-4347-8e26-dca240ffba08",
     "title": "Call summary"
    }
 }
\ No newline at end of file
diff --git a/providers/attio/test/write_tasks.json b/providers/attio/test/write_tasks.json
index 26f6107d..d91b06a6 100644
--- a/providers/attio/test/write_tasks.json
+++ b/providers/attio/test/write_tasks.json
@@ -1,28 +1,9 @@
 {
     "data": {
-    "assignees": [
-     {
-      "referenced_actor_id": "67af46e4-a450-4fee-a1d1-39729b3af771",
-      "referenced_actor_type": "workspace-member"
-     }
-    ],
     "content_plaintext": "view summary",
-    "created_at": "2024-10-04T10:03:04.216000000Z",
-    "created_by_actor": {
-     "id": "00bcd19c-9a89-467f-bf61-4e1a7f8b8754",
-     "type": "api-token"
-    },
-    "deadline_at": "2023-10-04T15:00:00.000000000Z",
     "id": {
      "task_id": "b38142c7-00f6-4d92-813e-7b0f689a5873",
      "workspace_id": "0d4d7fa2-d6e8-4a61-a7dc-e178405ff3c6"
-    },
-    "is_completed": false,
-    "linked_records": [
-     {
-      "target_object_id": "f4df082c-b46e-43e4-a747-f7918b487f44",
-      "target_record_id": "ec902ed9-aab7-4347-8e26-dca240ffba08"
-     }
-    ]
+    }
    }
 }
\ No newline at end of file
diff --git a/providers/attio/write_test.go b/providers/attio/write_test.go
index 50d3e774..d255c774 100644
--- a/providers/attio/write_test.go
+++ b/providers/attio/write_test.go
@@ -19,21 +19,14 @@ func TestWrite(t *testing.T) { // nolint:funlen,gocognit,cyclop
 	listResponse := testutils.DataFromFile(t, "write_lists.json")
 	notesresponse := testutils.DataFromFile(t, "write_notes.json")
 	tasksResponse := testutils.DataFromFile(t, "write_tasks.json")
+	companiesRecordResponse := testutils.DataFromFile(t, "write_companies_record.json")
+
 	tests := []testroutines.Write{
 		{
 			Name:         "Write object must be included",
 			Server:       mockserver.Dummy(),
 			ExpectedErrs: []error{common.ErrMissingObjects},
 		},
-		{
-			Name:     "Unknown object name is not supported",
-			Input:    common.WriteParams{ObjectName: "attributes", RecordData: "dummy"},
-			Server:   mockserver.Dummy(),
-			Expected: nil,
-			ExpectedErrs: []error{
-				common.ErrOperationNotSupportedForObject,
-			},
-		},
 		{
 			Name:  "Create lists as POST",
 			Input: common.WriteParams{ObjectName: "lists", RecordData: "dummy"},
@@ -47,27 +40,12 @@ func TestWrite(t *testing.T) { // nolint:funlen,gocognit,cyclop
 				RecordId: "e09a041c-0555-4bb2-8f6e-997bfc9b54e8",
 				Errors:   nil,
 				Data: map[string]any{
-					"api_slug":   "sales_investing",
-					"created_at": "2024-10-04T10:03:02.070000000Z",
-					"created_by_actor": map[string]any{
-						"id":   "00bcd19c-9a89-467f-bf61-4e1a7f8b8754",
-						"type": "api-token",
-					},
+					"api_slug": "sales_investing",
 					"id": map[string]any{
 						"list_id":      "e09a041c-0555-4bb2-8f6e-997bfc9b54e8",
 						"workspace_id": "0d4d7fa2-d6e8-4a61-a7dc-e178405ff3c6",
 					},
 					"name": "Sales",
-					"parent_object": []any{
-						"companies",
-					},
-					"workspace_access": "full-access",
-					"workspace_member_access": []any{
-						map[string]any{
-							"level":               "full-access",
-							"workspace_member_id": "67af46e4-a450-4fee-a1d1-39729b3af771",
-						},
-					},
 				},
 			},
 			ExpectedErrs: nil,
@@ -85,27 +63,12 @@ func TestWrite(t *testing.T) { // nolint:funlen,gocognit,cyclop
 				RecordId: "e09a041c-0555-4bb2-8f6e-997bfc9b54e8",
 				Errors:   nil,
 				Data: map[string]any{
-					"api_slug":   "sales_investing",
-					"created_at": "2024-10-04T10:03:02.070000000Z",
-					"created_by_actor": map[string]any{
-						"id":   "00bcd19c-9a89-467f-bf61-4e1a7f8b8754",
-						"type": "api-token",
-					},
+					"api_slug": "sales_investing",
 					"id": map[string]any{
 						"list_id":      "e09a041c-0555-4bb2-8f6e-997bfc9b54e8",
 						"workspace_id": "0d4d7fa2-d6e8-4a61-a7dc-e178405ff3c6",
 					},
 					"name": "Sales",
-					"parent_object": []any{
-						"companies",
-					},
-					"workspace_access": "full-access",
-					"workspace_member_access": []any{
-						map[string]any{
-							"level":               "full-access",
-							"workspace_member_id": "67af46e4-a450-4fee-a1d1-39729b3af771",
-						},
-					},
 				},
 			},
 			ExpectedErrs: nil,
@@ -124,18 +87,11 @@ func TestWrite(t *testing.T) { // nolint:funlen,gocognit,cyclop
 				Errors:   nil,
 				Data: map[string]any{
 					"content_plaintext": "summary",
-					"created_at":        "2024-10-04T10:03:03.347000000Z",
-					"created_by_actor": map[string]any{
-						"id":   "00bcd19c-9a89-467f-bf61-4e1a7f8b8754",
-						"type": "api-token",
-					},
 					"id": map[string]any{
 						"note_id":      "126e58a5-5e3f-4644-89ff-6474e97fcecd",
 						"workspace_id": "0d4d7fa2-d6e8-4a61-a7dc-e178405ff3c6",
 					},
-					"parent_object":    "companies",
-					"parent_record_id": "ec902ed9-aab7-4347-8e26-dca240ffba08",
-					"title":            "Call summary",
+					"title": "Call summary",
 				},
 			},
 			ExpectedErrs: nil,
@@ -153,30 +109,11 @@ func TestWrite(t *testing.T) { // nolint:funlen,gocognit,cyclop
 				RecordId: "b38142c7-00f6-4d92-813e-7b0f689a5873",
 				Errors:   nil,
 				Data: map[string]any{
-					"assignees": []any{
-						map[string]any{
-							"referenced_actor_id":   "67af46e4-a450-4fee-a1d1-39729b3af771",
-							"referenced_actor_type": "workspace-member",
-						},
-					},
 					"content_plaintext": "view summary",
-					"created_at":        "2024-10-04T10:03:04.216000000Z",
-					"created_by_actor": map[string]any{
-						"id":   "00bcd19c-9a89-467f-bf61-4e1a7f8b8754",
-						"type": "api-token",
-					},
-					"deadline_at": "2023-10-04T15:00:00.000000000Z",
 					"id": map[string]any{
 						"task_id":      "b38142c7-00f6-4d92-813e-7b0f689a5873",
 						"workspace_id": "0d4d7fa2-d6e8-4a61-a7dc-e178405ff3c6",
 					},
-					"is_completed": false,
-					"linked_records": []any{
-						map[string]any{
-							"target_object_id": "f4df082c-b46e-43e4-a747-f7918b487f44",
-							"target_record_id": "ec902ed9-aab7-4347-8e26-dca240ffba08",
-						},
-					},
 				},
 			},
 			ExpectedErrs: nil,
@@ -194,28 +131,81 @@ func TestWrite(t *testing.T) { // nolint:funlen,gocognit,cyclop
 				RecordId: "b38142c7-00f6-4d92-813e-7b0f689a5873",
 				Errors:   nil,
 				Data: map[string]any{
-					"assignees": []any{
-						map[string]any{
-							"referenced_actor_id":   "67af46e4-a450-4fee-a1d1-39729b3af771",
-							"referenced_actor_type": "workspace-member",
-						},
-					},
 					"content_plaintext": "view summary",
-					"created_at":        "2024-10-04T10:03:04.216000000Z",
-					"created_by_actor": map[string]any{
-						"id":   "00bcd19c-9a89-467f-bf61-4e1a7f8b8754",
-						"type": "api-token",
-					},
-					"deadline_at": "2023-10-04T15:00:00.000000000Z",
 					"id": map[string]any{
 						"task_id":      "b38142c7-00f6-4d92-813e-7b0f689a5873",
 						"workspace_id": "0d4d7fa2-d6e8-4a61-a7dc-e178405ff3c6",
 					},
-					"is_completed": false,
-					"linked_records": []any{
-						map[string]any{
-							"target_object_id": "f4df082c-b46e-43e4-a747-f7918b487f44",
-							"target_record_id": "ec902ed9-aab7-4347-8e26-dca240ffba08",
+				},
+			},
+			ExpectedErrs: nil,
+		},
+		{
+			Name:  "Create record for standard object as POST",
+			Input: common.WriteParams{ObjectName: "companies", RecordData: "dummy"},
+			Server: mockserver.Conditional{
+				Setup: mockserver.ContentJSON(),
+				If:    mockcond.MethodPOST(),
+				Then:  mockserver.Response(http.StatusOK, companiesRecordResponse),
+			}.Server(),
+			Expected: &common.WriteResult{
+				Success:  true,
+				RecordId: "238747c9-6dce-4475-8928-fe66ddfe8526",
+				Errors:   nil,
+				Data: map[string]any{
+					"id": map[string]any{
+						"workspace_id": "63d34516-b287-4c27-9d28-fe2adbebcd50",
+						"object_id":    "1fa986a6-952e-4e92-ba01-acca61a7b616",
+						"record_id":    "238747c9-6dce-4475-8928-fe66ddfe8526",
+					},
+					"values": map[string]any{
+						"name": []any{
+							map[string]any{
+								"active_from":  "2025-03-21T10:27:09.906000000Z",
+								"active_until": nil,
+								"created_by_actor": map[string]any{
+									"type": "api-token",
+									"id":   "0d91718e-9193-47e7-8b6c-6feeae40dcc1",
+								},
+								"value":          "pay",
+								"attribute_type": "text",
+							},
+						},
+					},
+				},
+			},
+			ExpectedErrs: nil,
+		},
+		{
+			Name:  "Update record for standard object as PUT",
+			Input: common.WriteParams{ObjectName: "companies", RecordId: "238747c9-6dce-4475-8928-fe66ddfe8526", RecordData: "dummy"},
+			Server: mockserver.Conditional{
+				Setup: mockserver.ContentJSON(),
+				If:    mockcond.MethodPUT(),
+				Then:  mockserver.Response(http.StatusOK, companiesRecordResponse),
+			}.Server(),
+			Expected: &common.WriteResult{
+				Success:  true,
+				RecordId: "238747c9-6dce-4475-8928-fe66ddfe8526",
+				Errors:   nil,
+				Data: map[string]any{
+					"id": map[string]any{
+						"workspace_id": "63d34516-b287-4c27-9d28-fe2adbebcd50",
+						"object_id":    "1fa986a6-952e-4e92-ba01-acca61a7b616",
+						"record_id":    "238747c9-6dce-4475-8928-fe66ddfe8526",
+					},
+					"values": map[string]any{
+						"name": []any{
+							map[string]any{
+								"active_from":  "2025-03-21T10:27:09.906000000Z",
+								"active_until": nil,
+								"created_by_actor": map[string]any{
+									"type": "api-token",
+									"id":   "0d91718e-9193-47e7-8b6c-6feeae40dcc1",
+								},
+								"value":          "pay",
+								"attribute_type": "text",
+							},
 						},
 					},
 				},
diff --git a/test/attio/write/main.go b/test/attio/write/main.go
index 4416c1c8..18960488 100644
--- a/test/attio/write/main.go
+++ b/test/attio/write/main.go
@@ -13,8 +13,8 @@ import (
 )
 
 const (
-	workspaceMemberID = "67af46e4-a450-4fee-a1d1-39729b3af771"
-	recordId          = "ec902ed9-aab7-4347-8e26-dca240ffba08"
+	workspaceMemberID = "073f4c74-b60d-4de9-992a-0f799b5e442e"
+	recordId          = "2db97cee-6c6b-4486-ae52-db8e4b6f44e9"
 )
 
 func main() {
@@ -39,6 +39,11 @@ func MainFn() int {
 		return 1
 	}
 
+	err = testCompanies(ctx)
+	if err != nil {
+		return 1
+	}
+
 	return 0
 }
 
@@ -52,8 +57,8 @@ func testLists(ctx context.Context) error {
 		RecordData: map[string]any{
 			"data": map[string]interface{}{
 				"workspace_access": "full-access",
-				"name":             "Marketing",
-				"api_slug":         "marketing_1",
+				"name":             "Recruiting",
+				"api_slug":         "recruiting",
 				"parent_object":    "companies",
 				"workspace_member_access": []map[string]string{
 					{
@@ -83,7 +88,7 @@ func testLists(ctx context.Context) error {
 		ObjectName: "lists",
 		RecordData: map[string]any{
 			"data": map[string]interface{}{
-				"name": "Sales",
+				"name": "Recruit",
 			},
 		},
 		RecordId: writeRes.Data["id"].(map[string]interface{})["list_id"].(string),
@@ -206,6 +211,64 @@ func testTasks(ctx context.Context) error {
 	return nil
 }
 
+func testCompanies(ctx context.Context) error {
+	conn := attio.GetAttioConnector(ctx)
+
+	slog.Info("Creating the record for Companies")
+
+	writeParams := common.WriteParams{
+		ObjectName: "companies",
+		RecordData: map[string]any{
+			"data": map[string]any{
+				"values": map[string]any{
+					"name":        "FireFox",
+					"domains":     []string{"firefox.com"},
+					"description": "Firefox is a free, open-source web browser developed by the Mozilla Corporation. It's known for its speed, privacy features, and customizable options, competing with browsers like Chrome and Safari.",
+					"categories":  []string{"SAAS", "Web Services & Apps", "Internet"},
+				},
+			},
+		},
+		RecordId: "",
+	}
+
+	writeRes, err := Write(ctx, conn, writeParams)
+	if err != nil {
+		fmt.Println("ERR: ", err)
+
+		return err
+	}
+
+	if err := constructResponse(writeRes); err != nil {
+		return err
+	}
+
+	slog.Info("Updating the record for Companies")
+
+	updateParams := common.WriteParams{
+		ObjectName: "companies",
+		RecordData: map[string]any{
+			"data": map[string]interface{}{
+				"values": map[string]any{
+					"categories": []string{"SAAS"},
+				},
+			},
+		},
+		RecordId: writeRes.Data["id"].(map[string]interface{})["record_id"].(string),
+	}
+
+	writeres, err := Write(ctx, conn, updateParams)
+	if err != nil {
+		fmt.Println("ERR: ", err)
+
+		return err
+	}
+
+	if err := constructResponse(writeres); err != nil {
+		return err
+	}
+
+	return nil
+}
 func Write(ctx context.Context, conn *ap.Connector, payload common.WriteParams) (*common.WriteResult, error) {
 	res, err := conn.Write(ctx, payload)
 	if err != nil {