From 8ea95c0d80067f48672c35dbb6d976ca1eec8de0 Mon Sep 17 00:00:00 2001 From: Cobalt0s Date: Thu, 7 May 2026 18:57:05 +0300 Subject: [PATCH 1/7] tidy(hubspot): Use only ProviderInfo.BaseURL and urlbuilder --- providers/hubspot/url.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/providers/hubspot/url.go b/providers/hubspot/url.go index cd814cdf0..4c2e966c5 100644 --- a/providers/hubspot/url.go +++ b/providers/hubspot/url.go @@ -98,3 +98,10 @@ func (c *Connector) getRootProviderURL() string { func (c *Connector) getDeleteURL(objectName, recordID string) (*urlbuilder.URL, error) { return urlbuilder.New(c.ModuleInfo().BaseURL, "objects", core.APIVersion2026March, objectName, recordID) } + +func (c *Connector) crmURL(paths ...string) (*urlbuilder.URL, error) { + parts := append([]string{"crm"}, paths...) + + // URL: "https://api.hubapi.com/crm" + return urlbuilder.New(c.ProviderInfo().BaseURL, parts...) +} From b5f63fdea23ae0e13c078e6209b3e82ab3a9bc5e Mon Sep 17 00:00:00 2001 From: Cobalt0s Date: Thu, 7 May 2026 19:15:28 +0300 Subject: [PATCH 2/7] (1) use urlbuilder --- providers/hubspot/read.go | 30 ++++++++++++---------------- providers/hubspot/record-count.go | 6 ++---- providers/hubspot/search.go | 8 ++++---- providers/hubspot/url.go | 33 ++++++++++++++----------------- 4 files changed, 34 insertions(+), 43 deletions(-) diff --git a/providers/hubspot/read.go b/providers/hubspot/read.go index 0e57c174a..b30b40540 100644 --- a/providers/hubspot/read.go +++ b/providers/hubspot/read.go @@ -88,34 +88,30 @@ func (c *Connector) Read(ctx context.Context, config common.ReadParams) (*common ) } -func (c *Connector) buildReadURL(config common.ReadParams) (string, error) { - if len(config.NextPage) != 0 { +func (c *Connector) buildReadURL(params common.ReadParams) (string, error) { + if len(params.NextPage) != 0 { // If NextPage is set, then we're reading the next page of results. // All that matters is the NextPage URL, the fields are ignored. - return config.NextPage.String(), nil + return params.NextPage.String(), nil } // If NextPage is not set, then we're reading the first page of results. // We need to construct the query and then make the request. - // NB: The final slash is just to emulate prior behavior in earlier versions - // of this code. If it turns out to be unnecessary, remove it. - return c.getCRMObjectsReadURL(config) -} - -// makeCRMObjectsQueryValues returns the query for the desired read operation. -func makeCRMObjectsQueryValues(config common.ReadParams) []string { - var out []string + url, err := c.getCRMObjectsReadURL(params.ObjectName) + if err != nil { + return "", err + } - fields := config.Fields.List() + fields := params.Fields.List() if len(fields) != 0 { - out = append(out, "properties", strings.Join(fields, ",")) + url.WithQueryParam("properties", strings.Join(fields, ",")) } - if config.Deleted { - out = append(out, "archived", "true") + if params.Deleted { + url.WithQueryParam("archived", "true") } - out = append(out, "limit", core.DefaultPageSize) + url.WithQueryParam("limit", core.DefaultPageSize) - return out + return url.String(), nil } diff --git a/providers/hubspot/record-count.go b/providers/hubspot/record-count.go index 6a07870e3..1265255f1 100644 --- a/providers/hubspot/record-count.go +++ b/providers/hubspot/record-count.go @@ -22,9 +22,7 @@ func (c *Connector) GetRecordCount( params *common.RecordCountParams, ) (*common.RecordCountResult, error) { // Build search URL - url, err := c.getCRMObjectsSearchURL(SearchParams{ - ObjectName: params.ObjectName, - }) + url, err := c.getCRMObjectsSearchURL(params.ObjectName) if err != nil { return nil, fmt.Errorf("failed to build search URL: %w", err) } @@ -61,7 +59,7 @@ func (c *Connector) GetRecordCount( } // Execute the search request - response, err := c.JSONHTTPClient().Post(ctx, url, filterBody) + response, err := c.JSONHTTPClient().Post(ctx, url.String(), filterBody) if err != nil { return nil, fmt.Errorf("failed to execute search request: %w", err) } diff --git a/providers/hubspot/search.go b/providers/hubspot/search.go index 16686e1da..11305a476 100644 --- a/providers/hubspot/search.go +++ b/providers/hubspot/search.go @@ -61,12 +61,12 @@ func (c *Connector) ReadUsingSearchAPI(ctx context.Context, config SearchParams) }) } - url, err := c.getCRMObjectsSearchURL(config) + url, err := c.getCRMObjectsSearchURL(config.ObjectName) if err != nil { return nil, err } - rsp, err := c.JSONHTTPClient().Post(ctx, url, makeFilterBody(config)) + rsp, err := c.JSONHTTPClient().Post(ctx, url.String(), makeFilterBody(config)) if err != nil { return nil, err } @@ -91,7 +91,7 @@ func (c *Connector) searchCRM( return nil, err } - url, err := c.getCRMSearchURL(config) + url, err := c.getCRMSearchURL(config.ObjectName) if err != nil { return nil, err } @@ -101,7 +101,7 @@ func (c *Connector) searchCRM( return nil, err } - rsp, err := c.JSONHTTPClient().Post(ctx, url, payload) + rsp, err := c.JSONHTTPClient().Post(ctx, url.String(), payload) if err != nil { return nil, err } diff --git a/providers/hubspot/url.go b/providers/hubspot/url.go index 4c2e966c5..3ba951c2a 100644 --- a/providers/hubspot/url.go +++ b/providers/hubspot/url.go @@ -7,7 +7,6 @@ import ( "path" "strings" - "github.com/amp-labs/connectors/common" "github.com/amp-labs/connectors/common/urlbuilder" "github.com/amp-labs/connectors/providers/hubspot/internal/core" ) @@ -17,6 +16,13 @@ var errMissingValue = errors.New("missing value for query parameter") // getURL is a helper to return the full URL considering the base URL & module. // TODO: replace queryArgs with urlbuilder.New().WithQueryParam(). func (c *Connector) getURL(arg string, queryArgs ...string) (string, error) { + // + // + // THIS is the same as + // ==> "c.crmURL(core.APIVersion3)" + // ==> plus, it adds query params + // + // baseURL := c.ModuleInfo().BaseURL ok := true @@ -48,31 +54,22 @@ func (c *Connector) getURL(arg string, queryArgs ...string) (string, error) { return urlBase, nil } -func (c *Connector) getCRMObjectsReadURL(config common.ReadParams) (string, error) { - // NB: The final slash is just to emulate prior behavior in earlier versions - // of this code. If it turns out to be unnecessary, remove it. - relativeURL := "objects/" + config.ObjectName + "/" - - // TODO c.getURL() doesn't make a module assumption. It is not important until Hubspot will have 2+ modules. - return c.getURL(relativeURL, makeCRMObjectsQueryValues(config)...) +func (c *Connector) getCRMObjectsReadURL(objectName string) (*urlbuilder.URL, error) { + return c.crmURL(core.APIVersion3, "objects", objectName) } -func (c *Connector) getCRMObjectsSearchURL(config SearchParams) (string, error) { - relativeURL := strings.Join([]string{"objects", config.ObjectName, "search"}, "/") - - return c.getURL(relativeURL) +func (c *Connector) getCRMObjectsSearchURL(objectName string) (*urlbuilder.URL, error) { + return c.crmURL(core.APIVersion3, "objects", objectName, "search") } -func (c *Connector) getCRMSearchURL(config searchCRMParams) (string, error) { - relativeURL := strings.Join([]string{config.ObjectName, "search"}, "/") - - return c.getURL(relativeURL) +func (c *Connector) getCRMSearchURL(objectName string) (*urlbuilder.URL, error) { + return c.crmURL(core.APIVersion3, objectName, "search") } // https://developers.hubspot.com/docs/api-reference/latest/crm/properties/get-properties // Note: Version APIVersion2026March is NOT FOUND at the moment for this endpoint. Using older V3. func (c *Connector) getPropertiesURL(objectName string) (*urlbuilder.URL, error) { - return urlbuilder.New(c.ModuleInfo().BaseURL, core.APIVersion3, "properties", objectName, "/") + return c.crmURL(core.APIVersion3, "properties", objectName, "/") } // https://developers.hubspot.com/docs/api-reference/latest/crm/objects/schemas/get-schema @@ -96,7 +93,7 @@ func (c *Connector) getRootProviderURL() string { // https://developers.hubspot.com/docs/api-reference/latest/crm/objects/contacts/delete-contact func (c *Connector) getDeleteURL(objectName, recordID string) (*urlbuilder.URL, error) { - return urlbuilder.New(c.ModuleInfo().BaseURL, "objects", core.APIVersion2026March, objectName, recordID) + return c.crmURL("objects", core.APIVersion2026March, objectName, recordID) } func (c *Connector) crmURL(paths ...string) (*urlbuilder.URL, error) { From 7c0c7a4173f2096922a51182c2d22acc198fb1a3 Mon Sep 17 00:00:00 2001 From: Cobalt0s Date: Thu, 7 May 2026 19:22:37 +0300 Subject: [PATCH 3/7] (2) stage remove outdated getURL --- providers/hubspot/record.go | 15 ++++--- providers/hubspot/url.go | 49 +-------------------- providers/hubspot/url_test.go | 81 ----------------------------------- providers/hubspot/write.go | 12 ++---- 4 files changed, 15 insertions(+), 142 deletions(-) delete mode 100644 providers/hubspot/url_test.go diff --git a/providers/hubspot/record.go b/providers/hubspot/record.go index 047898fa7..87893fc8a 100644 --- a/providers/hubspot/record.go +++ b/providers/hubspot/record.go @@ -56,7 +56,7 @@ func (c *Connector) GetRecordsByIds( pluralObjectName := naming.NewPluralString(objectName).String() - u, err := c.getBatchRecordsURL(pluralObjectName, associationsList) + u, err := c.buildBatchRecordsURL(pluralObjectName, associationsList) if err != nil { return nil, err } @@ -88,12 +88,15 @@ func (c *Connector) GetRecordsByIds( return marshaller(records, fields) } -func (c *Connector) getBatchRecordsURL(objectName string, associations []string) (string, error) { - relativePath := strings.Join([]string{"/objects", objectName, "batch", "read"}, "/") +func (c *Connector) buildBatchRecordsURL(objectName string, associations []string) (string, error) { + url, err := c.getBatchReadURL(objectName) + if err != nil { + return "", err + } if len(associations) > 0 { - return c.getURL(relativePath, "associations", strings.Join(associations, ",")) - } else { - return c.getURL(relativePath) + url.WithQueryParam("associations", strings.Join(associations, ",")) } + + return url.String(), nil } diff --git a/providers/hubspot/url.go b/providers/hubspot/url.go index 3ba951c2a..8e023d922 100644 --- a/providers/hubspot/url.go +++ b/providers/hubspot/url.go @@ -1,57 +1,12 @@ package hubspot import ( - "errors" - "fmt" - "net/url" - "path" - "strings" - "github.com/amp-labs/connectors/common/urlbuilder" "github.com/amp-labs/connectors/providers/hubspot/internal/core" ) -var errMissingValue = errors.New("missing value for query parameter") - -// getURL is a helper to return the full URL considering the base URL & module. -// TODO: replace queryArgs with urlbuilder.New().WithQueryParam(). -func (c *Connector) getURL(arg string, queryArgs ...string) (string, error) { - // - // - // THIS is the same as - // ==> "c.crmURL(core.APIVersion3)" - // ==> plus, it adds query params - // - // - baseURL := c.ModuleInfo().BaseURL - - ok := true - for ok { - // This is to satisfy the unit test, which states that trailing slashes should be removed. - baseURL, ok = strings.CutSuffix(baseURL, "/") - } - - urlBase := baseURL + "/" + path.Join(core.APIVersion3, arg) - - if len(queryArgs) > 0 { - vals := url.Values{} - - for i := 0; i < len(queryArgs); i += 2 { - key := queryArgs[i] - - if i+1 >= len(queryArgs) { - return "", fmt.Errorf("%w %q", errMissingValue, key) - } - - val := queryArgs[i+1] - - vals.Add(key, val) - } - - urlBase += "?" + vals.Encode() - } - - return urlBase, nil +func (c *Connector) getBatchReadURL(objectName string) (*urlbuilder.URL, error) { + return c.crmURL(core.APIVersion3, "objects", objectName, "batch", "read") } func (c *Connector) getCRMObjectsReadURL(objectName string) (*urlbuilder.URL, error) { diff --git a/providers/hubspot/url_test.go b/providers/hubspot/url_test.go deleted file mode 100644 index e86393f52..000000000 --- a/providers/hubspot/url_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package hubspot - -import ( - "testing" - - "github.com/amp-labs/connectors/common" - "github.com/amp-labs/connectors/providers" - "github.com/amp-labs/connectors/test/utils/mockutils" -) - -// nolint:funlen -func TestConnector_getURL_ModuleCRM(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - baseURL string // optional; if empty, use defaultBaseURL - arg string - queryArgs []string - wantURL string - wantErr bool - }{ - { - name: "Read with default baseURL (trailing slash removed)", - arg: "objects/contacts/", - wantURL: "https://api.hubapi.com/crm/v3/objects/contacts", - }, - { - name: "Read with query params (special chars)", - arg: "objects/contacts", - queryArgs: []string{"properties", "email,first name", "archived", "true"}, - wantURL: "https://api.hubapi.com/crm/v3/objects/contacts?archived=true&properties=email%2Cfirst+name", - }, - { - name: "Error: missing query param value", - arg: "objects/contacts", - queryArgs: []string{"properties"}, - wantErr: true, - }, - { - name: "BaseURL with extra trailing slash", - baseURL: "https://api.hubapi.com/crm/", // add an extra slash - arg: "objects/contacts/", - wantURL: "https://api.hubapi.com/crm/v3/objects/contacts", - }, - } - - for _, ttc := range cases { - t.Run(ttc.name, func(t *testing.T) { - t.Parallel() - - c, err := NewConnector( - common.ConnectorParams{ - Module: providers.ModuleHubspotCRM, - AuthenticatedClient: mockutils.NewClient(), - }, - ) - - if ttc.baseURL != "" { - c.ModuleInfo().BaseURL = ttc.baseURL - } - - gotURL, err := c.getURL(ttc.arg, ttc.queryArgs...) - if ttc.wantErr { - if err == nil { - t.Errorf("expected error, got nil") - } - - return - } - - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - if gotURL != ttc.wantURL { - t.Errorf("got URL %q, want %q", gotURL, ttc.wantURL) - } - }) - } -} diff --git a/providers/hubspot/write.go b/providers/hubspot/write.go index adcb4c564..1c225cc1c 100644 --- a/providers/hubspot/write.go +++ b/providers/hubspot/write.go @@ -2,8 +2,6 @@ package hubspot import ( "context" - "fmt" - "strings" "github.com/amp-labs/connectors/common" "github.com/amp-labs/connectors/common/logging" @@ -29,16 +27,14 @@ func (c *Connector) Write(ctx context.Context, config common.WriteParams) (*comm var write common.WriteMethod - relativeURL := strings.Join([]string{"objects", config.ObjectName}, "/") - - url, err := c.getURL(relativeURL) + url, err := c.getCRMObjectsReadURL(config.ObjectName) if err != nil { return nil, err } - if config.RecordId != "" { + if config.IsUpdate() { write = c.JSONHTTPClient().Patch - url = fmt.Sprintf("%s/%s", url, config.RecordId) + url.AddPath(config.RecordId) } else { write = c.JSONHTTPClient().Post } @@ -50,7 +46,7 @@ func (c *Connector) Write(ctx context.Context, config common.WriteParams) (*comm data["properties"] = config.RecordData data["associations"] = config.Associations - json, err := write(ctx, url, data) + json, err := write(ctx, url.String(), data) if err != nil { return nil, err } From 8c0356bff941225184661792276d2c4850eb327c Mon Sep 17 00:00:00 2001 From: Cobalt0s Date: Thu, 7 May 2026 19:27:02 +0300 Subject: [PATCH 4/7] (3) inline URL building --- providers/hubspot/url.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/providers/hubspot/url.go b/providers/hubspot/url.go index 8e023d922..9ed497a38 100644 --- a/providers/hubspot/url.go +++ b/providers/hubspot/url.go @@ -29,21 +29,16 @@ func (c *Connector) getPropertiesURL(objectName string) (*urlbuilder.URL, error) // https://developers.hubspot.com/docs/api-reference/latest/crm/objects/schemas/get-schema func (c *Connector) getObjectSchemaURL(objectName string) (*urlbuilder.URL, error) { - return urlbuilder.New(c.getRootProviderURL(), "crm-object-schemas", core.APIVersion2026March, "schemas", objectName) + return urlbuilder.New(c.ProviderInfo().BaseURL, "crm-object-schemas", core.APIVersion2026March, "schemas", objectName) } // https://developers.hubspot.com/docs/api-reference/latest/account/account-information/get-account-details func (c *Connector) getAccountDetailsURL() (*urlbuilder.URL, error) { - return urlbuilder.New(c.getRootProviderURL(), "account-info", core.APIVersion2026March, "details") + return urlbuilder.New(c.ProviderInfo().BaseURL, "account-info", core.APIVersion2026March, "details") } func (c *Connector) getURLFromRoot(relativePath string) string { - return c.getRootProviderURL() + relativePath -} - -// Returns module agnostic Hubspot URL. -func (c *Connector) getRootProviderURL() string { - return c.ProviderInfo().BaseURL + return c.ProviderInfo().BaseURL + relativePath } // https://developers.hubspot.com/docs/api-reference/latest/crm/objects/contacts/delete-contact From fd971fd2f2ddc3c9681ed982fbf7d7e34d9b32c3 Mon Sep 17 00:00:00 2001 From: Cobalt0s Date: Thu, 7 May 2026 19:39:06 +0300 Subject: [PATCH 5/7] (4) reorder --- providers/hubspot/url.go | 46 ++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/providers/hubspot/url.go b/providers/hubspot/url.go index 9ed497a38..cc7e3671c 100644 --- a/providers/hubspot/url.go +++ b/providers/hubspot/url.go @@ -5,20 +5,14 @@ import ( "github.com/amp-labs/connectors/providers/hubspot/internal/core" ) -func (c *Connector) getBatchReadURL(objectName string) (*urlbuilder.URL, error) { - return c.crmURL(core.APIVersion3, "objects", objectName, "batch", "read") -} - -func (c *Connector) getCRMObjectsReadURL(objectName string) (*urlbuilder.URL, error) { - return c.crmURL(core.APIVersion3, "objects", objectName) -} - -func (c *Connector) getCRMObjectsSearchURL(objectName string) (*urlbuilder.URL, error) { - return c.crmURL(core.APIVersion3, "objects", objectName, "search") +// https://developers.hubspot.com/docs/api-reference/latest/account/account-information/get-account-details +func (c *Connector) getAccountDetailsURL() (*urlbuilder.URL, error) { + return urlbuilder.New(c.ProviderInfo().BaseURL, "account-info", core.APIVersion2026March, "details") } -func (c *Connector) getCRMSearchURL(objectName string) (*urlbuilder.URL, error) { - return c.crmURL(core.APIVersion3, objectName, "search") +// https://developers.hubspot.com/docs/api-reference/latest/crm/objects/schemas/get-schema +func (c *Connector) getObjectSchemaURL(objectName string) (*urlbuilder.URL, error) { + return urlbuilder.New(c.ProviderInfo().BaseURL, "crm-object-schemas", core.APIVersion2026March, "schemas", objectName) } // https://developers.hubspot.com/docs/api-reference/latest/crm/properties/get-properties @@ -27,23 +21,25 @@ func (c *Connector) getPropertiesURL(objectName string) (*urlbuilder.URL, error) return c.crmURL(core.APIVersion3, "properties", objectName, "/") } -// https://developers.hubspot.com/docs/api-reference/latest/crm/objects/schemas/get-schema -func (c *Connector) getObjectSchemaURL(objectName string) (*urlbuilder.URL, error) { - return urlbuilder.New(c.ProviderInfo().BaseURL, "crm-object-schemas", core.APIVersion2026March, "schemas", objectName) +func (c *Connector) getCRMObjectsReadURL(objectName string) (*urlbuilder.URL, error) { + return c.crmURL(core.APIVersion3, "objects", objectName) } -// https://developers.hubspot.com/docs/api-reference/latest/account/account-information/get-account-details -func (c *Connector) getAccountDetailsURL() (*urlbuilder.URL, error) { - return urlbuilder.New(c.ProviderInfo().BaseURL, "account-info", core.APIVersion2026March, "details") +// https://developers.hubspot.com/docs/api-reference/latest/crm/objects/contacts/delete-contact +func (c *Connector) getDeleteURL(objectName, recordID string) (*urlbuilder.URL, error) { + return c.crmURL("objects", core.APIVersion2026March, objectName, recordID) } -func (c *Connector) getURLFromRoot(relativePath string) string { - return c.ProviderInfo().BaseURL + relativePath +func (c *Connector) getBatchReadURL(objectName string) (*urlbuilder.URL, error) { + return c.crmURL(core.APIVersion3, "objects", objectName, "batch", "read") } -// https://developers.hubspot.com/docs/api-reference/latest/crm/objects/contacts/delete-contact -func (c *Connector) getDeleteURL(objectName, recordID string) (*urlbuilder.URL, error) { - return c.crmURL("objects", core.APIVersion2026March, objectName, recordID) +func (c *Connector) getCRMObjectsSearchURL(objectName string) (*urlbuilder.URL, error) { + return c.crmURL(core.APIVersion3, "objects", objectName, "search") +} + +func (c *Connector) getCRMSearchURL(objectName string) (*urlbuilder.URL, error) { + return c.crmURL(core.APIVersion3, objectName, "search") } func (c *Connector) crmURL(paths ...string) (*urlbuilder.URL, error) { @@ -52,3 +48,7 @@ func (c *Connector) crmURL(paths ...string) (*urlbuilder.URL, error) { // URL: "https://api.hubapi.com/crm" return urlbuilder.New(c.ProviderInfo().BaseURL, parts...) } + +func (c *Connector) getURLFromRoot(relativePath string) string { + return c.ProviderInfo().BaseURL + relativePath +} From 2c396ec99913398ae2f990bf68be2d2fb0123cd2 Mon Sep 17 00:00:00 2001 From: Cobalt0s Date: Thu, 7 May 2026 20:12:40 +0300 Subject: [PATCH 6/7] (5) rename url functions, add docs --- providers/hubspot/delete.go | 2 +- providers/hubspot/metadata.go | 4 +- providers/hubspot/read.go | 2 +- providers/hubspot/record.go | 2 +- providers/hubspot/url.go | 81 ++++++++++++++++++++++++++++++++--- providers/hubspot/write.go | 2 +- 6 files changed, 81 insertions(+), 12 deletions(-) diff --git a/providers/hubspot/delete.go b/providers/hubspot/delete.go index bb349feac..11cde9c6f 100644 --- a/providers/hubspot/delete.go +++ b/providers/hubspot/delete.go @@ -10,7 +10,7 @@ import ( ) func (c *Connector) buildDeleteRequest(ctx context.Context, params common.DeleteParams) (*http.Request, error) { - url, err := c.getDeleteURL(params.ObjectName, params.RecordId) + url, err := c.getCRMObjectsDeleteURL(params.ObjectName, params.RecordId) if err != nil { return nil, err } diff --git a/providers/hubspot/metadata.go b/providers/hubspot/metadata.go index 98c5b4dd7..40387e272 100644 --- a/providers/hubspot/metadata.go +++ b/providers/hubspot/metadata.go @@ -122,7 +122,7 @@ func (c *Connector) getObjectMetadata(ctx context.Context, objectName string) (* func (c *Connector) getObjectMetadataFromPropertyAPI( ctx context.Context, objectName string, ) (*common.ObjectMetadata, error) { - url, err := c.getPropertiesURL(objectName) + url, err := c.getCRMPropertiesURL(objectName) if err != nil { return nil, err } @@ -519,7 +519,7 @@ type stage struct { func (c *Connector) fetchRequiredFieldsBestEffort( ctx context.Context, objectName string, fields map[string]common.FieldMetadata, ) (map[string]common.FieldMetadata, error) { - url, err := c.getObjectSchemaURL(objectName) + url, err := c.getCRMSchemaURL(objectName) if err != nil { return nil, err } diff --git a/providers/hubspot/read.go b/providers/hubspot/read.go index b30b40540..9687464bb 100644 --- a/providers/hubspot/read.go +++ b/providers/hubspot/read.go @@ -97,7 +97,7 @@ func (c *Connector) buildReadURL(params common.ReadParams) (string, error) { // If NextPage is not set, then we're reading the first page of results. // We need to construct the query and then make the request. - url, err := c.getCRMObjectsReadURL(params.ObjectName) + url, err := c.getCRMObjectsURL(params.ObjectName) if err != nil { return "", err } diff --git a/providers/hubspot/record.go b/providers/hubspot/record.go index 87893fc8a..edd3b63a0 100644 --- a/providers/hubspot/record.go +++ b/providers/hubspot/record.go @@ -89,7 +89,7 @@ func (c *Connector) GetRecordsByIds( } func (c *Connector) buildBatchRecordsURL(objectName string, associations []string) (string, error) { - url, err := c.getBatchReadURL(objectName) + url, err := c.getCRMObjectsBatchReadURL(objectName) if err != nil { return "", err } diff --git a/providers/hubspot/url.go b/providers/hubspot/url.go index cc7e3671c..68ed72c7e 100644 --- a/providers/hubspot/url.go +++ b/providers/hubspot/url.go @@ -5,39 +5,108 @@ import ( "github.com/amp-labs/connectors/providers/hubspot/internal/core" ) +// This file defines HubSpot endpoint builders used by this connector. +// +// HubSpot exposes several API families. Many connector operations live under +// the CRM namespace, but not all CRM endpoints belong to the HubSpot Objects API. +// +// In Ampersand terminology, "object" refers to a connector resource such as contacts or lists. +// In this file, "Objects" refers specifically to the HubSpot Objects API. +// It does not refer to an Ampersand object. +// +// +-----------------------------------+ +// | CRM | +// | +---------------------------+ | +// | | HubSpot Objects | | +// | | Contacts | | +// | | Leads | | +// | | Quotes | | +// | +---------------------------+ | +// | Lists | +// +-----------------------------------+ +// +// Example: +// +// - contacts belong to both the CRM namespace and the Objects API +// - lists belong to the CRM namespace but not to the Objects API +// +// The distinction matters because URL layouts differ between these endpoint families. + +// Used by GetPostAuthInfo. // https://developers.hubspot.com/docs/api-reference/latest/account/account-information/get-account-details func (c *Connector) getAccountDetailsURL() (*urlbuilder.URL, error) { return urlbuilder.New(c.ProviderInfo().BaseURL, "account-info", core.APIVersion2026March, "details") } +// Returns the schema endpoint for an object definition. +// +// Used to construct object metadata. +// Output: schemaResponse. +// // https://developers.hubspot.com/docs/api-reference/latest/crm/objects/schemas/get-schema -func (c *Connector) getObjectSchemaURL(objectName string) (*urlbuilder.URL, error) { +func (c *Connector) getCRMSchemaURL(objectName string) (*urlbuilder.URL, error) { return urlbuilder.New(c.ProviderInfo().BaseURL, "crm-object-schemas", core.APIVersion2026March, "schemas", objectName) } +// Returns the properties endpoint for an object. +// +// Used to construct object field metadata. +// Output: fieldDescriptionResponse. +// +// This endpoint does not currently expose APIVersion2026March, so it still uses v3. +// // https://developers.hubspot.com/docs/api-reference/latest/crm/properties/get-properties -// Note: Version APIVersion2026March is NOT FOUND at the moment for this endpoint. Using older V3. -func (c *Connector) getPropertiesURL(objectName string) (*urlbuilder.URL, error) { +func (c *Connector) getCRMPropertiesURL(objectName string) (*urlbuilder.URL, error) { return c.crmURL(core.APIVersion3, "properties", objectName, "/") } -func (c *Connector) getCRMObjectsReadURL(objectName string) (*urlbuilder.URL, error) { +// Returns the base HubSpot Objects API endpoint for CRUD operations. +// +// This URL shape is shared by CRUD operations. +// +// https://developers.hubspot.com/docs/api-reference/latest/crm/objects/contacts/get-contacts +// https://developers.hubspot.com/docs/api-reference/latest/crm/objects/contacts/create-contact +// https://developers.hubspot.com/docs/api-reference/latest/crm/objects/contacts/update-contact +// https://developers.hubspot.com/docs/api-reference/latest/crm/objects/contacts/delete-contact +// +// NOTE: the path layout still follows the older v3 structure. +func (c *Connector) getCRMObjectsURL(objectName string) (*urlbuilder.URL, error) { return c.crmURL(core.APIVersion3, "objects", objectName) } +// Returns the delete endpoint for the HubSpot Objects API. +// +// TODO: replace this helper with getCRMObjectsURL once getCRMObjectsURL is migrated to APIVersion2026March. +// // https://developers.hubspot.com/docs/api-reference/latest/crm/objects/contacts/delete-contact -func (c *Connector) getDeleteURL(objectName, recordID string) (*urlbuilder.URL, error) { +func (c *Connector) getCRMObjectsDeleteURL(objectName, recordID string) (*urlbuilder.URL, error) { return c.crmURL("objects", core.APIVersion2026March, objectName, recordID) } -func (c *Connector) getBatchReadURL(objectName string) (*urlbuilder.URL, error) { +// Returns the batch read endpoint for the HubSpot Objects API. +// +// https://developers.hubspot.com/docs/api-reference/latest/crm/objects/contacts/batch/get-contacts +// https://developers.hubspot.com/docs/api-reference/latest/crm/objects/leads/batch/get-leads +// https://developers.hubspot.com/docs/api-reference/latest/crm/objects/quotes/batch/get-quotes +func (c *Connector) getCRMObjectsBatchReadURL(objectName string) (*urlbuilder.URL, error) { return c.crmURL(core.APIVersion3, "objects", objectName, "batch", "read") } +// Returns the search endpoint for the HubSpot Objects API. +// +// https://developers.hubspot.com/docs/api-reference/latest/crm/objects/contacts/search/search-contacts +// https://developers.hubspot.com/docs/api-reference/latest/crm/objects/leads/search/search-leads +// https://developers.hubspot.com/docs/api-reference/latest/crm/objects/quotes/search/search-quotes func (c *Connector) getCRMObjectsSearchURL(objectName string) (*urlbuilder.URL, error) { return c.crmURL(core.APIVersion3, "objects", objectName, "search") } +// Returns a CRM search endpoint that does not belong to the Objects API. +// +// Some CRM resources, such as lists, live under CRM but outside the +// Objects API and therefore follow a different URL layout. +// +// https://developers.hubspot.com/docs/api-reference/latest/crm/lists/guide#retrieve-by-searching-list-details func (c *Connector) getCRMSearchURL(objectName string) (*urlbuilder.URL, error) { return c.crmURL(core.APIVersion3, objectName, "search") } diff --git a/providers/hubspot/write.go b/providers/hubspot/write.go index 1c225cc1c..1cd0e4c7d 100644 --- a/providers/hubspot/write.go +++ b/providers/hubspot/write.go @@ -27,7 +27,7 @@ func (c *Connector) Write(ctx context.Context, config common.WriteParams) (*comm var write common.WriteMethod - url, err := c.getCRMObjectsReadURL(config.ObjectName) + url, err := c.getCRMObjectsURL(config.ObjectName) if err != nil { return nil, err } From f8996e67bfbc3fbbef785706cb73965f1b858c3b Mon Sep 17 00:00:00 2001 From: Cobalt0s Date: Thu, 7 May 2026 20:21:46 +0300 Subject: [PATCH 7/7] (6) strategies rely on ProviderInfo --- providers/hubspot/connector.go | 10 ++++------ .../internal/associations/create_test.go | 2 +- .../hubspot/internal/associations/strategy.go | 10 +++------- providers/hubspot/internal/batch/adapter.go | 16 ++++++---------- providers/hubspot/internal/custom/adapter.go | 18 +++++++++--------- providers/hubspot/internal/search/strategy.go | 14 +++++--------- 6 files changed, 28 insertions(+), 42 deletions(-) diff --git a/providers/hubspot/connector.go b/providers/hubspot/connector.go index 7ab10f451..821950943 100644 --- a/providers/hubspot/connector.go +++ b/providers/hubspot/connector.go @@ -70,14 +70,12 @@ func constructor(base *components.Connector) (*Connector, error) { }, ) - connector.customAdapter = custom.NewAdapter(connector.JSONHTTPClient(), connector.ModuleInfo()) - associationsStrategy := associations.NewStrategy( - connector.JSONHTTPClient(), connector.ModuleInfo(), connector.ProviderInfo(), - ) + connector.customAdapter = custom.NewAdapter(connector.JSONHTTPClient(), connector.ProviderInfo()) + associationsStrategy := associations.NewStrategy(connector.JSONHTTPClient(), connector.ProviderInfo()) connector.associationsFiller = associationsStrategy - connector.batchAdapter = batch.NewAdapter(connector.HTTPClient(), connector.ModuleInfo(), associationsStrategy) + connector.batchAdapter = batch.NewAdapter(connector.HTTPClient(), connector.ProviderInfo(), associationsStrategy) connector.searchStrategy = search.NewStrategy( - connector.JSONHTTPClient(), connector.ModuleInfo(), connector.associationsFiller, + connector.JSONHTTPClient(), connector.ProviderInfo(), connector.associationsFiller, ) return connector, nil diff --git a/providers/hubspot/internal/associations/create_test.go b/providers/hubspot/internal/associations/create_test.go index 9a792809b..5cf073f83 100644 --- a/providers/hubspot/internal/associations/create_test.go +++ b/providers/hubspot/internal/associations/create_test.go @@ -196,7 +196,7 @@ func constructTestStrategy(serverURL string) (*Strategy, error) { transport.SetUnitTestMockServerBaseURL(serverURL) - return NewStrategy(transport.JSONHTTPClient(), transport.ModuleInfo(), transport.ProviderInfo()), nil + return NewStrategy(transport.JSONHTTPClient(), transport.ProviderInfo()), nil } type ( diff --git a/providers/hubspot/internal/associations/strategy.go b/providers/hubspot/internal/associations/strategy.go index 061e02488..2f486aea2 100644 --- a/providers/hubspot/internal/associations/strategy.go +++ b/providers/hubspot/internal/associations/strategy.go @@ -9,16 +9,12 @@ import ( type Strategy struct { clientCRM *common.JSONHTTPClient - moduleInfo *providers.ModuleInfo providerInfo *providers.ProviderInfo } -func NewStrategy( - hubspotCRMClient *common.JSONHTTPClient, moduleInfo *providers.ModuleInfo, providerInfo *providers.ProviderInfo, -) *Strategy { +func NewStrategy(hubspotCRMClient *common.JSONHTTPClient, providerInfo *providers.ProviderInfo) *Strategy { return &Strategy{ clientCRM: hubspotCRMClient, - moduleInfo: moduleInfo, providerInfo: providerInfo, } } @@ -28,7 +24,7 @@ func NewStrategy( // nolint:lll // https://developers.hubspot.com/docs/api-reference/latest/crm/associations/associate-records/batch/get-associations func (s Strategy) getReadAssociationsURL(fromObject, toObject string) (*urlbuilder.URL, error) { - return urlbuilder.New(s.moduleInfo.BaseURL, + return urlbuilder.New(s.providerInfo.BaseURL, "crm", "associations", core.APIVersion2026March, fromObject, toObject, "batch/read") } @@ -37,7 +33,7 @@ func (s Strategy) getReadAssociationsURL(fromObject, toObject string) (*urlbuild // nolint:lll // https://developers.hubspot.com/docs/api-reference/latest/crm/associations/associate-records/batch/create-associations-labeled func (s Strategy) getCreateAssociationsURL(fromObject, toObject string) (*urlbuilder.URL, error) { - return urlbuilder.New(s.moduleInfo.BaseURL, + return urlbuilder.New(s.providerInfo.BaseURL, "crm", "associations", core.APIVersion2026March, fromObject, toObject, "batch/create") } diff --git a/providers/hubspot/internal/batch/adapter.go b/providers/hubspot/internal/batch/adapter.go index 119a328b4..401f5a3e6 100644 --- a/providers/hubspot/internal/batch/adapter.go +++ b/providers/hubspot/internal/batch/adapter.go @@ -16,8 +16,8 @@ import ( // It abstracts API endpoint construction, versioning, and JSON response processing // specific to the HubSpot Batch feature. type Adapter struct { - Client *common.JSONHTTPClient - moduleInfo *providers.ModuleInfo + Client *common.JSONHTTPClient + providerInfo *providers.ProviderInfo // Batch updating objects does not support manipulating associations. // associationsStrategy is used to create associations as a follow up. @@ -26,7 +26,7 @@ type Adapter struct { // NewAdapter creates a new batch Adapter configured to work with Hubspot's APIs. func NewAdapter( - hubspotCRMClient *common.HTTPClient, moduleInfo *providers.ModuleInfo, + hubspotCRMClient *common.HTTPClient, providerInfo *providers.ProviderInfo, associationsStrategy *associations.Strategy, ) *Adapter { shouldHandleError := func(response *http.Response) bool { @@ -50,21 +50,17 @@ func NewAdapter( return &Adapter{ Client: jsonHTTPClient, - moduleInfo: moduleInfo, + providerInfo: providerInfo, associationsStrategy: associationsStrategy, } } -func (a *Adapter) getModuleURL() string { - return a.moduleInfo.BaseURL -} - // getCreateURL builds the HubSpot batch create endpoint for the given object type. // // nolint:lll // Contacts example: https://developers.hubspot.com/docs/api-reference/latest/crm/objects/contacts/batch/create-contacts func (a *Adapter) getCreateURL(objectName common.ObjectName) (*urlbuilder.URL, error) { - return urlbuilder.New(a.getModuleURL(), "objects", core.APIVersion2026March, objectName.String(), "batch/create") + return urlbuilder.New(a.providerInfo.BaseURL, "crm", "objects", core.APIVersion2026March, objectName.String(), "batch/create") } // getUpdateURL builds the HubSpot batch update endpoint for the given object type. @@ -72,5 +68,5 @@ func (a *Adapter) getCreateURL(objectName common.ObjectName) (*urlbuilder.URL, e // nolint:lll // Contacts example: https://developers.hubspot.com/docs/api-reference/latest/crm/objects/contacts/batch/update-contacts func (a *Adapter) getUpdateURL(objectName common.ObjectName) (*urlbuilder.URL, error) { - return urlbuilder.New(a.getModuleURL(), "objects", core.APIVersion2026March, objectName.String(), "batch/update") + return urlbuilder.New(a.providerInfo.BaseURL, "crm", "objects", core.APIVersion2026March, objectName.String(), "batch/update") } diff --git a/providers/hubspot/internal/custom/adapter.go b/providers/hubspot/internal/custom/adapter.go index 41e60e085..045d42b3c 100644 --- a/providers/hubspot/internal/custom/adapter.go +++ b/providers/hubspot/internal/custom/adapter.go @@ -8,37 +8,37 @@ import ( ) type Adapter struct { - Client *common.JSONHTTPClient - moduleInfo *providers.ModuleInfo + Client *common.JSONHTTPClient + providerInfo *providers.ProviderInfo } -func NewAdapter(client *common.JSONHTTPClient, moduleInfo *providers.ModuleInfo) *Adapter { +func NewAdapter(client *common.JSONHTTPClient, providerInfo *providers.ProviderInfo) *Adapter { return &Adapter{ - Client: client, - moduleInfo: moduleInfo, + Client: client, + providerInfo: providerInfo, } } // https://developers.hubspot.com/docs/api-reference/latest/crm/properties/batch/create-properties // Note: Version APIVersion2026March is NOT FOUND at the moment for this endpoint. Using older V3. func (a *Adapter) getPropertyBatchCreateURL(objectName string) (*urlbuilder.URL, error) { - return urlbuilder.New(a.moduleInfo.BaseURL, core.APIVersion3, "properties", objectName, "/batch/create") + return urlbuilder.New(a.providerInfo.BaseURL, "crm", core.APIVersion3, "properties", objectName, "/batch/create") } // https://developers.hubspot.com/docs/api-reference/latest/crm/properties/update-property // Note: Version APIVersion2026March is NOT FOUND at the moment for this endpoint. Using older V3. func (a *Adapter) getPropertyUpdateURL(objectName, propertyName string) (*urlbuilder.URL, error) { - return urlbuilder.New(a.moduleInfo.BaseURL, core.APIVersion3, "properties", objectName, propertyName) + return urlbuilder.New(a.providerInfo.BaseURL, "crm", core.APIVersion3, "properties", objectName, propertyName) } // https://developers.hubspot.com/docs/api-reference/latest/crm/properties/property-groups/get-property // Note: Version APIVersion2026March is NOT FOUND at the moment for this endpoint. Using older V3. func (a *Adapter) getPropertyGroupNameURL(objectName, groupName string) (*urlbuilder.URL, error) { - return urlbuilder.New(a.moduleInfo.BaseURL, core.APIVersion3, "properties", objectName, "groups", groupName) + return urlbuilder.New(a.providerInfo.BaseURL, "crm", core.APIVersion3, "properties", objectName, "groups", groupName) } // https://developers.hubspot.com/docs/api-reference/latest/crm/properties/property-groups/create-property // Note: Version APIVersion2026March is NOT FOUND at the moment for this endpoint. Using older V3. func (a *Adapter) getPropertyGroupNameCreationURL(objectName string) (*urlbuilder.URL, error) { - return urlbuilder.New(a.moduleInfo.BaseURL, core.APIVersion3, "properties", objectName, "groups") + return urlbuilder.New(a.providerInfo.BaseURL, "crm", core.APIVersion3, "properties", objectName, "groups") } diff --git a/providers/hubspot/internal/search/strategy.go b/providers/hubspot/internal/search/strategy.go index 4fe64d61f..725b28f1d 100644 --- a/providers/hubspot/internal/search/strategy.go +++ b/providers/hubspot/internal/search/strategy.go @@ -12,36 +12,32 @@ import ( type Strategy struct { clientCRM *common.JSONHTTPClient - moduleInfo *providers.ModuleInfo + providerInfo *providers.ProviderInfo associationsFiller associations.Filler } func NewStrategy( hubspotCRMClient *common.JSONHTTPClient, - moduleInfo *providers.ModuleInfo, + providerInfo *providers.ProviderInfo, associationsStrategy associations.Filler, ) *Strategy { return &Strategy{ clientCRM: hubspotCRMClient, // reuses error handling from Hubspot CRM connector. - moduleInfo: moduleInfo, + providerInfo: providerInfo, associationsFiller: associationsStrategy, } } -func (s Strategy) getModuleURL(paths ...string) (*urlbuilder.URL, error) { - return urlbuilder.New(s.moduleInfo.BaseURL, paths...) -} - // https://developers.hubspot.com/docs/api-reference/latest/crm/search-the-crm#make-a-search-request func (s Strategy) getObjectsAPISearchURL(objectName string) (*urlbuilder.URL, error) { - return s.getModuleURL("objects", core.APIVersion2026March, objectName, "search") + return urlbuilder.New(s.providerInfo.BaseURL, "crm", "objects", core.APIVersion2026March, objectName, "search") } func (s Strategy) getSearchURL(objectName string) (*urlbuilder.URL, error) { switch objectName { case "lists": // https://developers.hubspot.com/docs/api-reference/latest/crm/lists/guide#retrieve-by-searching-list-details - return s.getModuleURL("lists", core.APIVersion2026March, "search") + return urlbuilder.New(s.providerInfo.BaseURL, "crm", "lists", core.APIVersion2026March, "search") default: return nil, fmt.Errorf("%w: search not supported for %v", common.ErrObjectNotSupported, objectName) }