Repository: amp-labs/connectors
Pull request #1451 (2025-03-06 15:49:50)

[CON-1577] feat: Add GitHub Write + Delete Connector
# Testing 

## Write 
<img width="1115" alt="image" src="https://github.com/user-attachments/assets/81fa22bf-1d83-4ee8-a654-980fb49dc552" />
<img width="790" alt="image" src="https://github.com/user-attachments/assets/c16ddb91-29cd-48d8-ae0e-f5b7a35acc09" />


## Delete
<img width="684" alt="image" src="https://github.com/user-attachments/assets/26558e50-deb9-409e-aa31-f06cd6c21077" />



Patch:
diff --git a/providers/aha/connector.go b/providers/aha/connector.go
new file mode 100644
index 00000000..7237cde3
--- /dev/null
+++ b/providers/aha/connector.go
@@ -0,0 +1,54 @@
+package aha
+
+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/reader"
+	"github.com/amp-labs/connectors/internal/components/schema"
+	"github.com/amp-labs/connectors/providers"
+	"github.com/amp-labs/connectors/providers/aha/metadata"
+)
+
+type Connector struct {
+	// Basic connector
+	*components.Connector
+
+	// Require authenticated client
+	common.RequireAuthenticatedClient
+
+	// supported operations
+	components.SchemaProvider
+	components.Reader
+}
+
+func NewConnector(params common.Parameters) (*Connector, error) {
+	// Create base connector with provider info
+	return components.Initialize(providers.Aha, params, constructor)
+}
+
+//nolint:funlen
+func constructor(base *components.Connector) (*Connector, error) {
+	connector := &Connector{Connector: base}
+
+	// Set the metadata provider for the connector
+	connector.SchemaProvider = schema.NewOpenAPISchemaProvider(connector.ProviderContext.Module(), metadata.Schemas)
+
+	registry, err := components.NewEndpointRegistry(supportedOperations())
+	if err != nil {
+		return nil, err
+	}
+
+	connector.Reader = reader.NewHTTPReader(
+		connector.HTTPClient().Client,
+		registry,
+		connector.ProviderContext.Module(),
+		operations.ReadHandlers{
+			BuildRequest:  connector.buildReadRequest,
+			ParseResponse: connector.parseReadResponse,
+			ErrorHandler:  common.InterpretError,
+		},
+	)
+
+	return connector, nil
+}
diff --git a/providers/aha/handlers.go b/providers/aha/handlers.go
new file mode 100644
index 00000000..110a027e
--- /dev/null
+++ b/providers/aha/handlers.go
@@ -0,0 +1,108 @@
+package aha
+
+import (
+	"context"
+	"net/http"
+	"strconv"
+
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/common/urlbuilder"
+	"github.com/amp-labs/connectors/internal/datautils"
+	"github.com/amp-labs/connectors/internal/jsonquery"
+	"github.com/amp-labs/connectors/providers/aha/metadata"
+	"github.com/spyzhov/ajson"
+)
+
+const (
+	pageSizeKey = "per_page"
+	pageSize    = "200"
+	pageKey     = "page"
+	sinceKey    = "created_since"
+	apiVersion  = "v1"
+)
+
+func (c *Connector) buildReadRequest(ctx context.Context, params common.ReadParams) (*http.Request, error) {
+	if params.NextPage != "" {
+		return http.NewRequestWithContext(ctx, http.MethodGet, params.NextPage.String(), nil)
+	}
+
+	path, err := metadata.Schemas.LookupURLPath(c.Module(), params.ObjectName)
+	if err != nil {
+		return nil, err
+	}
+
+	url, err := urlbuilder.New(c.ProviderInfo().BaseURL, apiVersion, path)
+	if err != nil {
+		return nil, err
+	}
+
+	addQueryParams(url, params, 1)
+
+	return http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
+}
+
+func (c *Connector) parseReadResponse(
+	ctx context.Context,
+	params common.ReadParams,
+	request *http.Request,
+	response *common.JSONHTTPResponse,
+) (*common.ReadResult, error) {
+	responseKey := metadata.Schemas.LookupArrayFieldName(c.Module(), params.ObjectName)
+
+	path, err := metadata.Schemas.LookupURLPath(c.Module(), params.ObjectName)
+	if err != nil {
+		return nil, err
+	}
+
+	baseURL, err := urlbuilder.New(c.ProviderInfo().BaseURL, apiVersion, path)
+	if err != nil {
+		return nil, err
+	}
+
+	return common.ParseResult(
+		response,
+		common.GetRecordsUnderJSONPath(responseKey),
+		makeNextRecordsURL(baseURL, params),
+		common.GetMarshaledData,
+		params.Fields,
+	)
+}
+
+func makeNextRecordsURL(baseURL *urlbuilder.URL, params common.ReadParams) common.NextPageFunc {
+	return func(node *ajson.Node) (string, error) {
+		pagination, err := jsonquery.New(node).ObjectRequired("pagination")
+		if err != nil {
+			return "", nil //nolint:nilerr
+		}
+
+		totalPage, err := jsonquery.New(pagination).IntegerRequired("total_pages")
+		if err != nil {
+			return "", nil //nolint:nilerr
+		}
+
+		currentPage, err := jsonquery.New(pagination).IntegerRequired("current_page")
+		if err != nil {
+			return "", nil //nolint:nilerr
+		}
+
+		if currentPage == totalPage {
+			return "", nil //nolint:nilerr
+		}
+
+		nextPage := currentPage + 1
+
+		addQueryParams(baseURL, params, nextPage)
+
+		return baseURL.String(), nil
+	}
+}
+
+func addQueryParams(url *urlbuilder.URL, params common.ReadParams, page int64) {
+	url.WithQueryParam(pageSizeKey, pageSize)
+	url.WithQueryParam(pageKey, strconv.FormatInt(page, 10))
+
+	if supportSince.Has(params.ObjectName) {
+		// https://www.aha.io/api/resources/ideas/list_ideas
+		url.WithQueryParam(sinceKey, datautils.Time.FormatRFC3339inUTC(params.Since))
+	}
+}
diff --git a/providers/aha/metadata/metadata.go b/providers/aha/metadata/metadata.go
new file mode 100644
index 00000000..39b070a9
--- /dev/null
+++ b/providers/aha/metadata/metadata.go
@@ -0,0 +1,20 @@
+package metadata
+
+import (
+	_ "embed"
+
+	"github.com/amp-labs/connectors/internal/staticschema"
+	"github.com/amp-labs/connectors/tools/fileconv"
+	"github.com/amp-labs/connectors/tools/scrapper"
+)
+
+// There is no OpenAPI available. The schemas.json file is created manually.
+// nolint:gochecknoglobals
+var (
+	//go:embed schemas.json
+	schemaContent []byte
+	FileManager   = scrapper.NewMetadataFileManager[staticschema.FieldMetadataMapV2](
+		schemaContent, fileconv.NewSiblingFileLocator())
+
+	Schemas = FileManager.MustLoadSchemas()
+)
diff --git a/providers/aha/metadata/schemas.json b/providers/aha/metadata/schemas.json
new file mode 100644
index 00000000..740268a9
--- /dev/null
+++ b/providers/aha/metadata/schemas.json
@@ -0,0 +1,1483 @@
+{
+    "modules": {
+        "root": {
+            "id": "root",
+            "path": "",
+            "objects": {
+                "audits": {
+                    "displayName": "Audits",
+                    "path": "/audits",
+                    "responseKey": "audits",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "audit_action": {
+                            "displayName": "Audit Action",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "created_at": {
+                            "displayName": "Created At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "interesting": {
+                            "displayName": "Interesting",
+                            "valueType": "boolean",
+                            "providerType": "boolean"
+                        },
+                        "user": {
+                            "displayName": "User",
+                            "valueType": "other",
+                            "providerType": "object"
+                        },
+                        "contributors": {
+                            "displayName": "Contributors",
+                            "valueType": "other",
+                            "providerType": "array"
+                        },
+                        "auditable_type": {
+                            "displayName": "Auditable Type",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "auditable_id": {
+                            "displayName": "Auditable Id",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "associated_type": {
+                            "displayName": "Associated Type",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "associated_id": {
+                            "displayName": "Associated Id",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "description": {
+                            "displayName": "Description",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "auditable_url": {
+                            "displayName": "Auditable Url",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "changes": {
+                            "displayName": "Changes",
+                            "valueType": "other",
+                            "providerType": "array"
+                        }
+                    }
+                },
+                "capacity_scenarios": {
+                    "displayName": "Capacity Scenarios",
+                    "path": "/capacity_scenarios",
+                    "responseKey": "capacity_scenarios",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "planning_interval": {
+                            "displayName": "Planning Interval",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "planning_start_date": {
+                            "displayName": "Planning Start Date",
+                            "valueType": "date",
+                            "providerType": "date"
+                        },
+                        "planning_end_date": {
+                            "displayName": "Planning End Date",
+                            "valueType": "date",
+                            "providerType": "date"
+                        },
+                        "estimate_value_layout": {
+                            "displayName": "Estimate Value Layout",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "data_entry_units": {
+                            "displayName": "Data Entry Units",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "archived": {
+                            "displayName": "Archived",
+                            "valueType": "boolean",
+                            "providerType": "boolean"
+                        }
+                    }
+                },
+                "screen_definitions": {
+                    "displayName": "Screen Definitions",
+                    "path": "/screen_definitions",
+                    "responseKey": "screen_definitions",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "screenable_type": {
+                            "displayName": "Screenable Type",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "custom_field_definitions": {
+                            "displayName": "Custom Field Definitions",
+                            "valueType": "other",
+                            "providerType": "array"
+                        }
+                    }
+                },
+                "custom_field_definitions": {
+                    "displayName": "Custom Field Definitions",
+                    "path": "/custom_field_definitions",
+                    "responseKey": "custom_field_definitions",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "key": {
+                            "displayName": "Key",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "type": {
+                            "displayName": "Type",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "custom_fieldable_type": {
+                            "displayName": "Custom Fieldable Type",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "internal_name": {
+                            "displayName": "Internal Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        }
+                    }
+                },
+                "epics": {
+                    "displayName": "Epics",
+                    "path": "/epics",
+                    "responseKey": "epics",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "reference_num": {
+                            "displayName": "Reference Num",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "created_at": {
+                            "displayName": "Created At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "url": {
+                            "displayName": "Url",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "resource": {
+                            "displayName": "Resource",
+                            "valueType": "string",
+                            "providerType": "string"
+                        }
+                    }
+                },
+                "features": {
+                    "displayName": "Features",
+                    "path": "/features",
+                    "responseKey": "features",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "reference_num": {
+                            "displayName": "Reference Num",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "created_at": {
+                            "displayName": "Created At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "url": {
+                            "displayName": "Url",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "resource": {
+                            "displayName": "Resource",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "product_id": {
+                            "displayName": "Product Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        }
+                    }
+                },
+                "goals": {
+                    "displayName": "Goals",
+                    "path": "/goals",
+                    "responseKey": "goals",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "reference_num": {
+                            "displayName": "Reference Num",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "effort": {
+                            "displayName": "Effort",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "value": {
+                            "displayName": "Value",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "color": {
+                            "displayName": "Color",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "position": {
+                            "displayName": "Position",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "created_at": {
+                            "displayName": "Created At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "updated_at": {
+                            "displayName": "Updated At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "product_id": {
+                            "displayName": "Product Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "progress": {
+                            "displayName": "Progress",
+                            "valueType": "float",
+                            "providerType": "number"
+                        },
+                        "progress_source": {
+                            "displayName": "Progress Source",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "url": {
+                            "displayName": "Url",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "resource": {
+                            "displayName": "Resource",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "description": {
+                            "displayName": "Description",
+                            "valueType": "object",
+                            "providerType": "object"
+                        },
+                        "success_metric": {
+                            "displayName": "Success Metric",
+                            "valueType": "object",
+                            "providerType": "object"
+                        },
+                        "project": {
+                            "displayName": "Project",
+                            "valueType": "object",
+                            "providerType": "object"
+                        },
+                        "parent": {
+                            "displayName": "Parent",
+                            "valueType": "object",
+                            "providerType": "object"
+                        },
+                        "parents": {
+                            "displayName": "Parents",
+                            "valueType": "other",
+                            "providerType": "array"
+                        },
+                        "initiatives": {
+                            "displayName": "Initiatives",
+                            "valueType": "other",
+                            "providerType": "array"
+                        },
+                        "key_results": {
+                            "displayName": "Key Results",
+                            "valueType": "other",
+                            "providerType": "array"
+                        },
+                        "comments_count": {
+                            "displayName": "Comments Count",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "features": {
+                            "displayName": "Features",
+                            "valueType": "other",
+                            "providerType": "array"
+                        },
+                        "releases": {
+                            "displayName": "Releases",
+                            "valueType": "other",
+                            "providerType": "array"
+                        },
+                        "custom_fields": {
+                            "displayName": "Custom Fields",
+                            "valueType": "other",
+                            "providerType": "array"
+                        }
+                    }
+                },
+                "historical_audits": {
+                    "displayName": "Historical Audits",
+                    "path": "/historical_audits",
+                    "responseKey": "historical_audits",
+                    "fields": {
+                        "auditable_id": {
+                            "displayName": "Auditable Id",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "auditable_type": {
+                            "displayName": "Auditable Type",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "email": {
+                            "displayName": "Email",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "created_at": {
+                            "displayName": "Created At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "updated_at": {
+                            "displayName": "Updated At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "dates_active": {
+                            "displayName": "Dates Active",
+                            "valueType": "other",
+                            "providerType": "array"
+                        }
+                    }
+                },
+                "idea_portals": {
+                    "displayName": "Idea Portals",
+                    "path": "/idea_portals",
+                    "responseKey": "idea_portals",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "title": {
+                            "displayName": "Title",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "portal_enabled": {
+                            "displayName": "Portal Enabled",
+                            "valueType": "boolean",
+                            "providerType": "boolean"
+                        },
+                        "access_type": {
+                            "displayName": "Access Type",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "external_url": {
+                            "displayName": "External Url",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "created_at": {
+                            "displayName": "Created At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        }
+                    }
+                },
+                "idea_organizations": {
+                    "displayName": "Idea Organizations",
+                    "path": "/idea_organizations",
+                    "responseKey": "idea_organizations",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "created_at": {
+                            "displayName": "Created At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "url": {
+                            "displayName": "Url",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "resource": {
+                            "displayName": "Resource",
+                            "valueType": "string",
+                            "providerType": "string"
+                        }
+                    }
+                },
+                "idea_users": {
+                    "displayName": "Idea Users",
+                    "path": "/idea_users",
+                    "responseKey": "idea_users",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "email": {
+                            "displayName": "Email",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "created_at": {
+                            "displayName": "Created At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "idea_organizations": {
+                            "displayName": "Idea Organizations",
+                            "valueType": "other",
+                            "providerType": "array"
+                        },
+                        "custom_fields": {
+                            "displayName": "Custom Fields",
+                            "valueType": "other",
+                            "providerType": "array"
+                        }
+                    }
+                },
+                "ideas/endorsements": {
+                    "displayName": "Ideas Endorsements",
+                    "path": "/ideas/endorsements",
+                    "responseKey": "idea_endorsements",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "idea_id": {
+                            "displayName": "Idea Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "created_at": {
+                            "displayName": "Created At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "updated_at": {
+                            "displayName": "Updated At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "value": {
+                            "displayName": "Value",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "link": {
+                            "displayName": "Link",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "weight": {
+                            "displayName": "Weight",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "endorsed_by_portal_user": {
+                            "displayName": "Endorsed By Portal User",
+                            "valueType": "object",
+                            "providerType": "object"
+                        },
+                        "endorsed_by_idea_user": {
+                            "displayName": "Endorsed By Idea User",
+                            "valueType": "object",
+                            "providerType": "object"
+                        },
+                        "idea_organization": {
+                            "displayName": "Idea Organization",
+                            "valueType": "object",
+                            "providerType": "object"
+                        }
+                    }
+                },
+                "ideas": {
+                    "displayName": "Ideas",
+                    "path": "/ideas",
+                    "responseKey": "ideas",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "reference_num": {
+                            "displayName": "Reference Num",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "created_at": {
+                            "displayName": "Created At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "updated_at": {
+                            "displayName": "Updated At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "workflow_status": {
+                            "displayName": "Workflow Status",
+                            "valueType": "object",
+                            "providerType": "object"
+                        },
+                        "description": {
+                            "displayName": "Description",
+                            "valueType": "object",
+                            "providerType": "object"
+                        },
+                        "url": {
+                            "displayName": "Url",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "resource": {
+                            "displayName": "Resource",
+                            "valueType": "string",
+                            "providerType": "string"
+                        }
+                    }
+                },
+                "identity_providers": {
+                    "displayName": "Identity Providers",
+                    "path": "/identity_providers",
+                    "responseKey": "identity_providers",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "enabled": {
+                            "displayName": "Enabled",
+                            "valueType": "boolean",
+                            "providerType": "boolean"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "sso_endpoint": {
+                            "displayName": "SSO Endpoint",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "created_at": {
+                            "displayName": "Created At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "updated_at": {
+                            "displayName": "Updated At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        }
+                    }
+                },
+                "initiatives": {
+                    "displayName": "Initiatives",
+                    "path": "/initiatives",
+                    "responseKey": "initiatives",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "reference_num": {
+                            "displayName": "Reference Num",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "status": {
+                            "displayName": "Status",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "effort": {
+                            "displayName": "Effort",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "value": {
+                            "displayName": "Value",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "presented": {
+                            "displayName": "Presented",
+                            "valueType": "boolean",
+                            "providerType": "boolean"
+                        },
+                        "color": {
+                            "displayName": "Color",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "start_date": {
+                            "displayName": "Start Date",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "end_date": {
+                            "displayName": "End Date",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "position": {
+                            "displayName": "Position",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "score": {
+                            "displayName": "Score",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "created_at": {
+                            "displayName": "Created At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "updated_at": {
+                            "displayName": "Updated At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "product_id": {
+                            "displayName": "Product Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "progress": {
+                            "displayName": "Progress",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "progress_source": {
+                            "displayName": "Progress Source",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "duration_source": {
+                            "displayName": "Duration Source",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "url": {
+                            "displayName": "Url",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "resource": {
+                            "displayName": "Resource",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "project": {
+                            "displayName": "Project",
+                            "valueType": "object",
+                            "providerType": "object"
+                        },
+                        "workflow_status": {
+                            "displayName": "Workflow Status",
+                            "valueType": "object",
+                            "providerType": "object"
+                        },
+                        "description": {
+                            "displayName": "Description",
+                            "valueType": "object",
+                            "providerType": "object"
+                        },
+                        "attachments": {
+                            "displayName": "Attachments",
+                            "valueType": "other",
+                            "providerType": "array"
+                        },
+                        "assigned_to_user": {
+                            "displayName": "Assigned To User",
+                            "valueType": "object",
+                            "providerType": "object"
+                        },
+                        "comments_count": {
+                            "displayName": "Comments Count",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "goals": {
+                            "displayName": "Goals",
+                            "valueType": "other",
+                            "providerType": "array"
+                        },
+                        "key_results": {
+                            "displayName": "Key Results",
+                            "valueType": "other",
+                            "providerType": "array"
+                        },
+                        "score_facts": {
+                            "displayName": "Score Facts",
+                            "valueType": "other",
+                            "providerType": "array"
+                        },
+                        "integration_fields": {
+                            "displayName": "Integration Fields",
+                            "valueType": "other",
+                            "providerType": "array"
+                        },
+                        "created_by_user": {
+                            "displayName": "Created By User",
+                            "valueType": "object",
+                            "providerType": "object"
+                        }
+                    }
+                },
+                "integrations": {
+                    "displayName": "Integrations",
+                    "path": "/integrations",
+                    "responseKey": "integrations",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "service_name": {
+                            "displayName": "Service Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "created_at": {
+                            "displayName": "Created At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        }
+                    }
+                },
+                "me/tasks": {
+                    "displayName": "My Tasks",
+                    "path": "/me/tasks",
+                    "responseKey": "tasks",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "due_date": {
+                            "displayName": "Due Date",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "status": {
+                            "displayName": "Status",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "created_at": {
+                            "displayName": "Created At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        }
+                    }
+                },
+                "me/assigned": {
+                    "displayName": "My Assignments",
+                    "path": "/me/assigned",
+                    "responseKey": "assigned",
+                    "fields": {
+                        "record_type": {
+                            "displayName": "Record Type",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "reference_num": {
+                            "displayName": "Reference Num",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "workflow_status": {
+                            "displayName": "Workflow Status",
+                            "valueType": "object",
+                            "providerType": "object"
+                        }
+                    }
+                },
+                "paid_seat_groups": {
+                    "displayName": "Paid Seat Groups",
+                    "path": "/paid_seat_groups",
+                    "responseKey": "paid_seat_groups",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "administrators": {
+                            "displayName": "Administrators",
+                            "valueType": "other",
+                            "providerType": "array"
+                        },
+                        "capacity": {
+                            "displayName": "Capacity",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "allocated_seats": {
+                            "displayName": "Allocated Seats",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "description": {
+                            "displayName": "Description",
+                            "valueType": "object",
+                            "providerType": "object"
+                        }
+                    }
+                },
+                "products": {
+                    "displayName": "Products",
+                    "path": "/products",
+                    "responseKey": "products",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "reference_prefix": {
+                            "displayName": "Reference Prefix",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "product_line": {
+                            "displayName": "Product Line",
+                            "valueType": "boolean",
+                            "providerType": "boolean"
+                        },
+                        "created_at": {
+                            "displayName": "Created At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "workspace_type": {
+                            "displayName": "Workspace Type",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "url": {
+                            "displayName": "Url",
+                            "valueType": "string",
+                            "providerType": "string"
+                        }
+                    }
+                },
+                "release_phases": {
+                    "displayName": "Release Phases",
+                    "path": "/release_phases",
+                    "responseKey": "release_phases",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "start_on": {
+                            "displayName": "Start On",
+                            "valueType": "date",
+                            "providerType": "date"
+                        },
+                        "end_on": {
+                            "displayName": "End On",
+                            "valueType": "date",
+                            "providerType": "date"
+                        },
+                        "type": {
+                            "displayName": "Type",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "release_id": {
+                            "displayName": "Release Id",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "created_at": {
+                            "displayName": "Created At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "updated_at": {
+                            "displayName": "Updated At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "progress": {
+                            "displayName": "Progress",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "progress_source": {
+                            "displayName": "Progress Source",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "duration_source": {
+                            "displayName": "Duration Source",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "description": {
+                            "displayName": "Description",
+                            "valueType": "object",
+                            "providerType": "object"
+                        }
+                    }
+                },
+                "schedules": {
+                    "displayName": "Schedules",
+                    "path": "/schedules",
+                    "responseKey": "schedules",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "hours_per_day": {
+                            "displayName": "Hours Per Day",
+                            "valueType": "float",
+                            "providerType": "number"
+                        },
+                        "story_points_per_day": {
+                            "displayName": "Story Points Per Day",
+                            "valueType": "float",
+                            "providerType": "number"
+                        },
+                        "velocity": {
+                            "displayName": "Velocity",
+                            "valueType": "float",
+                            "providerType": "number"
+                        }
+                    }
+                },
+                "strategy_models": {
+                    "displayName": "Strategy Models",
+                    "path": "/strategy_models",
+                    "responseKey": "strategy_models",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "kind": {
+                            "displayName": "Kind",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "components": {
+                            "displayName": "Components",
+                            "valueType": "other",
+                            "providerType": "array"
+                        },
+                        "url": {
+                            "displayName": "Url",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "resource": {
+                            "displayName": "Resource",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "project": {
+                            "displayName": "Project",
+                            "valueType": "object",
+                            "providerType": "object"
+                        }
+                    }
+                },
+                "strategy_positions": {
+                    "displayName": "Strategy Positions",
+                    "path": "/strategy_positions",
+                    "responseKey": "strategy_positions",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "kind": {
+                            "displayName": "Kind",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "components": {
+                            "displayName": "Components",
+                            "valueType": "other",
+                            "providerType": "array"
+                        },
+                        "url": {
+                            "displayName": "Url",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "resource": {
+                            "displayName": "Resource",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "project": {
+                            "displayName": "Project",
+                            "valueType": "object",
+                            "providerType": "object"
+                        }
+                    }
+                },
+                "strategy_visions": {
+                    "displayName": "Strategy Visions",
+                    "path": "/strategy_visions",
+                    "responseKey": "strategy_visions",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "created_at": {
+                            "displayName": "Created At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "updated_at": {
+                            "displayName": "Updated At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        },
+                        "description": {
+                            "displayName": "Description",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "strategy_vision_components": {
+                            "displayName": "Strategy Vision Components",
+                            "valueType": "other",
+                            "providerType": "array"
+                        },
+                        "url": {
+                            "displayName": "Url",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "resource": {
+                            "displayName": "Resource",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "project": {
+                            "displayName": "Project",
+                            "valueType": "object",
+                            "providerType": "object"
+                        }
+                    }
+                },
+                "team_members": {
+                    "displayName": "Team Members",
+                    "path": "/team_members",
+                    "responseKey": "team_members",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "email": {
+                            "displayName": "Email",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "user_id": {
+                            "displayName": "User Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "virtual": {
+                            "displayName": "Virtual",
+                            "valueType": "boolean",
+                            "providerType": "boolean"
+                        }
+                    }
+                },
+                "teams": {
+                    "displayName": "Teams",
+                    "path": "/teams",
+                    "responseKey": "teams",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "team_members_count": {
+                            "displayName": "Team Members Count",
+                            "valueType": "int",
+                            "providerType": "integer"
+                        },
+                        "automatically_calculate_team_members_count": {
+                            "displayName": "Automatically Calculate Team Members Count",
+                            "valueType": "boolean",
+                            "providerType": "boolean"
+                        },
+                        "capacity": {
+                            "displayName": "Capacity",
+                            "valueType": "float",
+                            "providerType": "number"
+                        },
+                        "hourly_rate": {
+                            "displayName": "Hourly Rate",
+                            "valueType": "float",
+                            "providerType": "number"
+                        },
+                        "color": {
+                            "displayName": "Color",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "start_date": {
+                            "displayName": "Start Date",
+                            "valueType": "date",
+                            "providerType": "date"
+                        },
+                        "end_date": {
+                            "displayName": "End Date",
+                            "valueType": "date",
+                            "providerType": "date"
+                        },
+                        "project": {
+                            "displayName": "Project",
+                            "valueType": "object",
+                            "providerType": "object"
+                        },
+                        "schedule": {
+                            "displayName": "Schedule",
+                            "valueType": "object",
+                            "providerType": "object"
+                        },
+                        "custom_fields": {
+                            "displayName": "Custom Fields",
+                            "valueType": "other",
+                            "providerType": "array"
+                        },
+                        "team_members": {
+                            "displayName": "Team Members",
+                            "valueType": "object",
+                            "providerType": "object"
+                        }
+                    }
+                },
+                "tasks": {
+                    "displayName": "Tasks",
+                    "path": "/tasks",
+                    "responseKey": "tasks",
+                    "fields": {
+                        "id": {
+                            "displayName": "Id",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "name": {
+                            "displayName": "Name",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "due_date": {
+                            "displayName": "Due Date",
+                            "valueType": "date",
+                            "providerType": "date"
+                        },
+                        "status": {
+                            "displayName": "Status",
+                            "valueType": "string",
+                            "providerType": "string"
+                        },
+                        "created_at": {
+                            "displayName": "Created At",
+                            "valueType": "datetime",
+                            "providerType": "datetime"
+                        }
+                    }
+                },
+                "users": {
+                    "displayName": "Users",
+                    "path": "/users",
+                    "responseKey": "users",
+                    "fields": {
+                      "id": {
+                        "displayName": "Id",
+                        "valueType": "string",
+                        "providerType": "string"
+                      },
+                      "name": {
+                        "displayName": "Name",
+                        "valueType": "string",
+                        "providerType": "string"
+                      },
+                      "email": {
+                        "displayName": "Email",
+                        "valueType": "string",
+                        "providerType": "string"
+                      },
+                      "created_at": {
+                        "displayName": "Created At",
+                        "valueType": "datetime",
+                        "providerType": "datetime"
+                      },
+                      "updated_at": {
+                        "displayName": "Updated At",
+                        "valueType": "datetime",
+                        "providerType": "datetime"
+                      },
+                      "accessed_at": {
+                        "displayName": "Accessed At",
+                        "valueType": "datetime",
+                        "providerType": "datetime"
+                      },
+                      "product_roles": {
+                        "displayName": "Product Roles",
+                        "valueType": "other",
+                        "providerType": "array"
+                      },
+                      "user_roles": {
+                        "displayName": "User Roles",
+                        "valueType": "other",
+                        "providerType": "array"
+                      },
+                      "enabled": {
+                        "displayName": "Enabled",
+                        "valueType": "boolean",
+                        "providerType": "boolean"
+                      },
+                      "paid_seat": {
+                        "displayName": "Paid Seat",
+                        "valueType": "boolean",
+                        "providerType": "boolean"
+                      },
+                      "administrator": {
+                        "displayName": "Administrator",
+                        "valueType": "boolean",
+                        "providerType": "boolean"
+                      },
+                      "administrator_roles": {
+                        "displayName": "Administrator Roles",
+                        "valueType": "object",
+                        "providerType": "object"
+                      },
+                      "paid_seat_group": {
+                        "displayName": "Paid Seat Group",
+                        "valueType": "object",
+                        "providerType": "object"
+                      },
+                      "identity_provider": {
+                        "displayName": "Identity Provider",
+                        "valueType": "object",
+                        "providerType": "object"
+                      }
+                    }
+                  }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/providers/aha/supports.go b/providers/aha/supports.go
new file mode 100644
index 00000000..f13a3ea9
--- /dev/null
+++ b/providers/aha/supports.go
@@ -0,0 +1,38 @@
+package aha
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/amp-labs/connectors/internal/components"
+	"github.com/amp-labs/connectors/internal/datautils"
+	"github.com/amp-labs/connectors/internal/staticschema"
+	"github.com/amp-labs/connectors/providers/aha/metadata"
+)
+
+const (
+	objectNameAudits            = "audits"
+	objectNameHistoricalAudits  = "historical_audits"
+	objectNameIdeasEndorsements = "ideas/endorsements"
+	objectNameIdeas             = "ideas"
+)
+
+var supportSince = datautils.NewSet( //nolint:gochecknoglobals
+	objectNameAudits,
+	objectNameHistoricalAudits,
+	objectNameIdeasEndorsements,
+	objectNameIdeas,
+)
+
+func supportedOperations() components.EndpointRegistryInput {
+	readSupport := metadata.Schemas.ObjectNames().GetList(staticschema.RootModuleID)
+
+	return components.EndpointRegistryInput{
+		staticschema.RootModuleID: {
+			{
+				Endpoint: fmt.Sprintf("{%s}", strings.Join(readSupport, ",")),
+				Support:  components.ReadSupport,
+			},
+		},
+	}
+}
diff --git a/providers/github/connector.go b/providers/github/connector.go
index a2814986..7eb9c2e0 100644
--- a/providers/github/connector.go
+++ b/providers/github/connector.go
@@ -3,9 +3,11 @@ package github
 import (
 	"github.com/amp-labs/connectors/common"
 	"github.com/amp-labs/connectors/internal/components"
+	"github.com/amp-labs/connectors/internal/components/deleter"
 	"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"
 	"github.com/amp-labs/connectors/providers/github/metadata"
 )
@@ -20,6 +22,8 @@ type Connector struct {
 	// supported operations
 	components.SchemaProvider
 	components.Reader
+	components.Writer
+	components.Deleter
 }
 
 func NewConnector(params common.Parameters) (*Connector, error) {
@@ -50,5 +54,27 @@ 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,
+		},
+	)
+
+	connector.Deleter = deleter.NewHTTPDeleter(
+		connector.HTTPClient().Client,
+		registry,
+		connector.ProviderContext.Module(),
+		operations.DeleteHandlers{
+			BuildRequest:  connector.buildDeleteRequest,
+			ParseResponse: connector.parseDeleteResponse,
+			ErrorHandler:  common.InterpretError,
+		},
+	)
+
 	return connector, nil
 }
diff --git a/providers/github/handlers.go b/providers/github/handlers.go
index a263b691..8aae646f 100644
--- a/providers/github/handlers.go
+++ b/providers/github/handlers.go
@@ -1,13 +1,17 @@
 package github
 
 import (
+	"bytes"
 	"context"
+	"encoding/json"
+	"fmt"
 	"net/http"
 
 	"github.com/amp-labs/connectors/common"
 	"github.com/amp-labs/connectors/common/urlbuilder"
 	"github.com/amp-labs/connectors/internal/datautils"
 	"github.com/amp-labs/connectors/internal/httpkit"
+	"github.com/amp-labs/connectors/internal/jsonquery"
 	"github.com/amp-labs/connectors/providers/github/metadata"
 	"github.com/spyzhov/ajson"
 )
@@ -73,3 +77,93 @@ func makeNextRecordsURL(responseHeaders http.Header) common.NextPageFunc {
 		return nextURL, nil
 	}
 }
+
+func (c *Connector) buildWriteRequest(ctx context.Context, params common.WriteParams) (*http.Request, error) { //nolint:lll
+	url, err := urlbuilder.New(c.ProviderInfo().BaseURL, params.ObjectName)
+	if err != nil {
+		return nil, err
+	}
+
+	method := http.MethodPost
+
+	if params.RecordId != "" {
+		if !supportByUpdate.Has(params.ObjectName) {
+			return nil, common.ErrOperationNotSupportedForObject
+		}
+
+		url.AddPath(params.RecordId)
+
+		method = http.MethodPatch
+	}
+
+	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
+	}
+
+	data, err := jsonquery.Convertor.ObjectToMap(body)
+	if err != nil { //nolint:nilerr
+		return &common.WriteResult{
+			Success: true,
+			Errors:  nil,
+			Data:    nil,
+		}, nil
+	}
+
+	recordID, err := jsonquery.New(body).StrWithDefault("id", "")
+	if err != nil { // nolint:nilerr
+		return &common.WriteResult{
+			Success: true,
+			Errors:  nil,
+			Data:    data,
+		}, nil
+	}
+
+	return &common.WriteResult{
+		Success:  true,
+		RecordId: recordID,
+		Errors:   nil,
+		Data:     data,
+	}, nil
+}
+
+func (c *Connector) buildDeleteRequest(ctx context.Context, params common.DeleteParams) (*http.Request, error) {
+	url, err := urlbuilder.New(c.ProviderInfo().BaseURL, params.ObjectName, params.RecordId)
+	if err != nil {
+		return nil, err
+	}
+
+	return http.NewRequestWithContext(ctx, http.MethodDelete, url.String(), nil)
+}
+
+func (c *Connector) parseDeleteResponse(
+	ctx context.Context,
+	params common.DeleteParams,
+	request *http.Request,
+	response *common.JSONHTTPResponse,
+) (*common.DeleteResult, error) {
+	if response.Code != http.StatusOK && response.Code != http.StatusNoContent {
+		return nil, fmt.Errorf("%w: failed to delete record: %d", common.ErrRequestFailed, response.Code)
+	}
+
+	// A successful delete returns 200 OK
+	return &common.DeleteResult{
+		Success: true,
+	}, nil
+}
diff --git a/providers/github/supports.go b/providers/github/supports.go
index 0f6a0adc..03630bf1 100644
--- a/providers/github/supports.go
+++ b/providers/github/supports.go
@@ -10,6 +10,45 @@ import (
 	"github.com/amp-labs/connectors/providers/github/metadata"
 )
 
+//nolint:lll,gochecknoglobals
+var (
+	// https://docs.github.com/en/rest/gists/gists?apiVersion=2022-11-28#create-a-gist
+	// https://docs.github.com/en/rest/gists/gists?apiVersion=2022-11-28#delete-a-gist
+	objectNameGist = "gists"
+
+	// https://docs.github.com/en/rest/users/emails?apiVersion=2022-11-28#add-an-email-address-for-the-authenticated-user
+	objectNameUserEmails = "user/emails"
+
+	// https://docs.github.com/en/rest/users/gpg-keys?apiVersion=2022-11-28#create-a-gpg-key-for-the-authenticated-user
+	// https://docs.github.com/en/rest/users/gpg-keys?apiVersion=2022-11-28#delete-a-gpg-key-for-the-authenticated-user
+	objectNameUserGpgKeys = "user/gpg_keys"
+
+	// https://docs.github.com/en/rest/users/keys?apiVersion=2022-11-28#create-a-public-ssh-key-for-the-authenticated-user
+	// https://docs.github.com/en/rest/users/keys?apiVersion=2022-11-28#delete-a-public-ssh-key-for-the-authenticated-user
+	objectNameUserKeys = "user/keys"
+
+	// https://docs.github.com/en/rest/users/ssh-signing-keys?apiVersion=2022-11-28#create-a-ssh-signing-key-for-the-authenticated-user
+	// https://docs.github.com/en/rest/users/ssh-signing-keys?apiVersion=2022-11-28#delete-an-ssh-signing-key-for-the-authenticated-user
+	objectNameUserSSHSigningKeys = "user/ssh_signing_keys"
+
+	// https://docs.github.com/en/rest/users/social-accounts?apiVersion=2022-11-28#add-social-accounts-for-the-authenticated-user
+	objectNameUserSocialAccounts = "user/social_accounts"
+
+	// https://docs.github.com/en/rest/users/followers?apiVersion=2022-11-28#unfollow-a-user
+	objectNameUserFollowing = "user/following"
+
+	// https://docs.github.com/en/rest/users/blocking?apiVersion=2022-11-28#unblock-a-user
+	objectNameUserBlocks = "user/blocks"
+
+	// https://docs.github.com/en/rest/orgs/orgs?apiVersion=2022-11-28#delete-an-organization
+	objectNameOrgs = "orgs"
+
+	// https://docs.github.com/en/rest/codespaces/codespaces?apiVersion=2022-11-28#create-a-codespace-for-the-authenticated-user
+	// https://docs.github.com/en/rest/codespaces/codespaces?apiVersion=2022-11-28#update-a-codespace-for-the-authenticated-user
+	// https://docs.github.com/en/rest/codespaces/codespaces?apiVersion=2022-11-28#delete-a-codespace-for-the-authenticated-user
+	objectNameUserCodespaces = "user/codespaces"
+)
+
 var (
 	supportPagination = datautils.NewSet( //nolint: gochecknoglobals
 		"advisories", "blocks", "classrooms", "codespaces",
@@ -27,17 +66,41 @@ var (
 		"gists", "gists/starred", "issues",
 		"gists/public", "repos", "user/issues",
 	)
+
+	supportByUpdate = datautils.NewSet( //nolint: gochecknoglobals
+		objectNameGist, objectNameUserCodespaces,
+	)
 )
 
 func supportedOperations() components.EndpointRegistryInput {
 	readSupport := metadata.Schemas.ObjectNames().GetList(staticschema.RootModuleID)
 
+	// nolint:lll
+	writeSupport := []string{
+		objectNameGist, objectNameUserEmails, objectNameUserGpgKeys, objectNameUserKeys, objectNameUserSSHSigningKeys, objectNameUserSocialAccounts,
+		objectNameUserCodespaces,
+	}
+
+	// nolint:lll
+	deleteSupport := []string{
+		objectNameUserGpgKeys, objectNameUserKeys, objectNameUserSSHSigningKeys, objectNameUserFollowing, objectNameUserEmails, objectNameUserBlocks,
+		objectNameOrgs, objectNameGist, objectNameUserCodespaces,
+	}
+
 	return components.EndpointRegistryInput{
 		staticschema.RootModuleID: {
 			{
 				Endpoint: fmt.Sprintf("{%s}", strings.Join(readSupport, ",")),
 				Support:  components.ReadSupport,
 			},
+			{
+				Endpoint: fmt.Sprintf("{%s}", strings.Join(writeSupport, ",")),
+				Support:  components.WriteSupport,
+			},
+			{
+				Endpoint: fmt.Sprintf("{%s}", strings.Join(deleteSupport, ",")),
+				Support:  components.DeleteSupport,
+			},
 		},
 	}
 }


Test patch:
diff --git a/test/aha/connector.go b/test/aha/connector.go
new file mode 100644
index 00000000..1c704bc4
--- /dev/null
+++ b/test/aha/connector.go
@@ -0,0 +1,44 @@
+package aha
+
+import (
+	"context"
+
+	"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/aha"
+	"github.com/amp-labs/connectors/test/utils"
+	"golang.org/x/oauth2"
+)
+
+func GetAhaConnector(ctx context.Context) *aha.Connector {
+	filePath := credscanning.LoadPath(providers.Aha)
+	reader := utils.MustCreateProvCredJSON(filePath, true, true)
+
+	conn, err := aha.NewConnector(
+		common.Parameters{
+			AuthenticatedClient: utils.NewOauth2Client(ctx, reader, getConfig),
+			Workspace:           reader.Get(credscanning.Fields.Workspace),
+		},
+	)
+	if err != nil {
+		utils.Fail("error creating connector", "error", err)
+	}
+
+	return conn
+}
+
+func getConfig(reader *credscanning.ProviderCredentials) *oauth2.Config {
+
+	return &oauth2.Config{
+		ClientID:     reader.Get(credscanning.Fields.ClientId),
+		ClientSecret: reader.Get(credscanning.Fields.ClientSecret),
+
+		RedirectURL: "https://api.withampersand.com/callbacks/v1/oauth",
+		Endpoint: oauth2.Endpoint{
+			AuthURL:   "https://{{.workspace}}.aha.io/oauth/authorize",
+			TokenURL:  "https://{{.workspace}}.aha.io/oauth/token",
+			AuthStyle: oauth2.AuthStyleAutoDetect,
+		},
+	}
+}
diff --git a/test/aha/metadata/metadata.go b/test/aha/metadata/metadata.go
new file mode 100644
index 00000000..28752b7b
--- /dev/null
+++ b/test/aha/metadata/metadata.go
@@ -0,0 +1,24 @@
+package main
+
+import (
+	"context"
+	"log"
+	"os"
+
+	"github.com/amp-labs/connectors/test/aha"
+	"github.com/amp-labs/connectors/test/utils"
+)
+
+func main() {
+	ctx := context.Background()
+
+	conn := aha.GetAhaConnector(ctx)
+
+	m, err := conn.ListObjectMetadata(ctx, []string{"audits", "idea_organizations", "me/assigned"})
+
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	utils.DumpJSON(m, os.Stdout)
+}
diff --git a/test/aha/read/main.go b/test/aha/read/main.go
new file mode 100644
index 00000000..236d107b
--- /dev/null
+++ b/test/aha/read/main.go
@@ -0,0 +1,60 @@
+package main
+
+import (
+	"context"
+	"log/slog"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
+
+	"github.com/amp-labs/connectors"
+	"github.com/amp-labs/connectors/common"
+	connTest "github.com/amp-labs/connectors/test/aha"
+	"github.com/amp-labs/connectors/test/utils"
+)
+
+func main() {
+	// Handle Ctrl-C gracefully.
+	ctx, done := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+	defer done()
+
+	// Set up slog logging.
+	utils.SetupLogging()
+
+	conn := connTest.GetAhaConnector(ctx)
+
+	res, err := conn.Read(ctx, common.ReadParams{
+		ObjectName: "audits",
+		Fields:     connectors.Fields("id", "created_at", "user"),
+		Since:      time.Now().AddDate(-1, 0, 0),
+	})
+	if err != nil {
+		utils.Fail("error reading from Aha", "error", err)
+	}
+
+	slog.Info("Reading audits..")
+	utils.DumpJSON(res, os.Stdout)
+
+	res, err = conn.Read(ctx, common.ReadParams{
+		ObjectName: "screen_definitions",
+		Fields:     connectors.Fields("id", "name", "custom_field_definitions", "screenable_type"),
+	})
+	if err != nil {
+		utils.Fail("error reading from Aha", "error", err)
+	}
+
+	slog.Info("Reading screen_definitions..")
+	utils.DumpJSON(res, os.Stdout)
+
+	res, err = conn.Read(ctx, common.ReadParams{
+		ObjectName: "custom_field_definitions",
+		Fields:     connectors.Fields("id", "name", "key", "type"),
+	})
+	if err != nil {
+		utils.Fail("error reading from Aha", "error", err)
+	}
+
+	slog.Info("Reading custom_field_definitions..")
+	utils.DumpJSON(res, os.Stdout)
+}
diff --git a/test/github/delete/main.go b/test/github/delete/main.go
new file mode 100644
index 00000000..7a66be93
--- /dev/null
+++ b/test/github/delete/main.go
@@ -0,0 +1,53 @@
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"log/slog"
+	"os/signal"
+	"syscall"
+
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/providers/github"
+	hs "github.com/amp-labs/connectors/test/github"
+	"github.com/amp-labs/connectors/test/utils"
+)
+
+func main() {
+	// Handle Ctrl-C gracefully.
+	ctx, done := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+	defer done()
+
+	// Set up slog logging.
+	utils.SetupLogging()
+
+	conn := hs.GetGithubConnector(ctx)
+
+	slog.Info("> TEST Delete gists")
+	if err := deleteGist(ctx, conn); err != nil {
+		slog.Error(err.Error())
+	}
+
+}
+
+func deleteGist(ctx context.Context, conn *github.Connector) error {
+	config := common.DeleteParams{
+		ObjectName: "gists",
+		RecordId:   "d4e45740e4c6edd1a329436244718fc2",
+	}
+
+	result, err := conn.Delete(ctx, config)
+	if err != nil {
+		return err
+	}
+
+	jsonStr, err := json.MarshalIndent(result, "", "  ")
+	if err != nil {
+		return err
+	}
+
+	fmt.Println(string(jsonStr))
+
+	return nil
+}
diff --git a/test/github/write/main.go b/test/github/write/main.go
new file mode 100644
index 00000000..cbf60bde
--- /dev/null
+++ b/test/github/write/main.go
@@ -0,0 +1,119 @@
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"log/slog"
+	"os/signal"
+	"syscall"
+
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/providers/github"
+	hs "github.com/amp-labs/connectors/test/github"
+	"github.com/amp-labs/connectors/test/utils"
+)
+
+func main() {
+	// Handle Ctrl-C gracefully.
+	ctx, done := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+	defer done()
+
+	// Set up slog logging.
+	utils.SetupLogging()
+
+	conn := hs.GetGithubConnector(ctx)
+
+	slog.Info("> TEST Add User Emails")
+
+	if err := addUserEmails(ctx, conn); err != nil {
+		slog.Error(err.Error())
+	}
+
+	slog.Info("> TEST Create gists")
+	if err := createGist(ctx, conn); err != nil {
+		slog.Error(err.Error())
+	}
+
+	slog.Info("> TEST update user codespaces")
+	if err := updateUserCodespaces(ctx, conn); err != nil {
+		slog.Error(err.Error())
+	}
+
+}
+
+func addUserEmails(ctx context.Context, conn *github.Connector) error {
+	config := common.WriteParams{
+		ObjectName: "user/emails",
+		RecordData: map[string]any{
+			"emails": []string{
+				"testagain@withmampersand.com",
+			},
+		},
+	}
+
+	result, err := conn.Write(ctx, config)
+	if err != nil {
+		return err
+	}
+
+	jsonStr, err := json.MarshalIndent(result, "", "  ")
+	if err != nil {
+		return err
+	}
+
+	fmt.Println(string(jsonStr))
+
+	return nil
+}
+
+func createGist(ctx context.Context, conn *github.Connector) error {
+	config := common.WriteParams{
+		ObjectName: "gists",
+		RecordData: map[string]any{
+			"description": "Example of a gist",
+			"public":      false,
+			"files": map[string]any{
+				"README.md": map[string]string{"content": "Hello World"},
+			},
+		},
+	}
+
+	result, err := conn.Write(ctx, config)
+	if err != nil {
+		return err
+	}
+
+	jsonStr, err := json.MarshalIndent(result, "", "  ")
+	if err != nil {
+		return err
+	}
+
+	fmt.Println(string(jsonStr))
+
+	return nil
+}
+
+func updateUserCodespaces(ctx context.Context, conn *github.Connector) error {
+	config := common.WriteParams{
+		ObjectName: "user/codespaces",
+		RecordId:   "fuzzy-fishstick-jj457pqw4q7g2565v",
+		RecordData: map[string]any{
+			"display_name": "test",
+		},
+	}
+
+	result, err := conn.Write(ctx, config)
+	if err != nil {
+		return err
+	}
+
+	jsonStr, err := json.MarshalIndent(result, "", "  ")
+	if err != nil {
+		return err
+	}
+
+	fmt.Println(string(jsonStr))
+
+	return nil
+}