From 02ed782e5d90c3012e88ebc83be8a1bc3b84b9ab Mon Sep 17 00:00:00 2001 From: Stephen Blackstone Date: Fri, 31 Mar 2023 14:21:18 -0400 Subject: [PATCH] Add support for properties and schemas endpoints --- contact.go | 2 +- crm.go | 16 +++- crm_properties.go | 106 ++++++++++++++++++++++ crm_properties_test.go | 84 ++++++++++++++++++ crm_schemas.go | 105 ++++++++++++++++++++++ crm_schemas_test.go | 194 +++++++++++++++++++++++++++++++++++++++++ gohubspot.go | 4 +- type.go | 7 ++ 8 files changed, 512 insertions(+), 6 deletions(-) create mode 100644 crm_properties.go create mode 100644 crm_properties_test.go create mode 100644 crm_schemas.go create mode 100644 crm_schemas_test.go diff --git a/contact.go b/contact.go index 93902ae..171cc92 100644 --- a/contact.go +++ b/contact.go @@ -355,7 +355,7 @@ func (s *ContactServiceOp) Update(contactID string, contact interface{}) (*Respo // Delete deletes a contact. func (s *ContactServiceOp) Delete(contactID string) error { - return s.client.Delete(s.contactPath + "/" + contactID) + return s.client.Delete(s.contactPath+"/"+contactID, nil) } // AssociateAnotherObj associates Contact with another HubSpot objects. diff --git a/crm.go b/crm.go index b502ffb..7eac54a 100644 --- a/crm.go +++ b/crm.go @@ -9,9 +9,11 @@ const ( ) type CRM struct { - Contact ContactService - Deal DealService - Imports CrmImportsService + Contact ContactService + Deal DealService + Imports CrmImportsService + Schemas CrmSchemasService + Properties CrmPropertiesService } func newCRM(c *Client) *CRM { @@ -29,5 +31,13 @@ func newCRM(c *Client) *CRM { crmImportsPath: fmt.Sprintf("%s/%s", crmPath, crmImportsBasePath), client: c, }, + Schemas: &CrmSchemasServiceOp{ + crmSchemasPath: fmt.Sprintf("%s/%s", crmPath, crmSchemasPath), + client: c, + }, + Properties: &CrmPropertiesServiceOp{ + crmPropertiesPath: fmt.Sprintf("%s/%s", crmPath, crmPropertiesPath), + client: c, + }, } } diff --git a/crm_properties.go b/crm_properties.go new file mode 100644 index 0000000..8030622 --- /dev/null +++ b/crm_properties.go @@ -0,0 +1,106 @@ +package hubspot + +import ( + "fmt" +) + +const ( + crmPropertiesPath = "properties" +) + +type CrmPropertiesList struct { + Results []CrmProperty +} + +type CrmProperty struct { + Calculated *HsBool `json:"calculated"` + Description *HsStr `json:"description"` + DisplayOrder *HsInt `json:"displayOrder"` + ExternalOptions *HsBool `json:"externalOptions"` + FieldType *HsStr `json:"fieldType"` + FormField *HsBool `json:"formField"` + GroupName *HsStr `json:"groupName"` + HasUniqueValue *HsBool `json:"hasUniqueValue"` + Hidden *HsBool `json:"hidden"` + HubspotDefined *HsBool `json:"hubspotDefined"` + Label *HsStr `json:"label"` + ModificationMeta *CrmPropertyModificationMeta + Name *HsStr `json:"name"` + Options []*CrmPropertyOptions + Type *HsStr `json:"type"` + CreatedUserId *HsStr + UpdatedUserId *HsStr + CreatedAt *HsTime + UpdatedAt *HsTime +} + +type CrmPropertyModificationMeta struct { + Archivable *HsBool `json:"archivable"` + ReadOnlyDefition *HsBool `json:"readOnlyDefinition"` + ReadOnlyValue *HsBool `json:"readOnlyValue"` +} +type CrmPropertyOptions struct { + DisplayOrder *HsInt `json:"displayOrder"` + Hidden *HsBool `json:"hidden"` + Label *HsStr `json:"label"` + Value *HsStr `json:"value"` +} + +// CrmPropertiesService is an interface of CRM properties endpoints of the HubSpot API. +// Reference: https://developers.hubspot.com/docs/api/crm/properties +type CrmPropertiesService interface { + List(string) (*CrmPropertiesList, error) + Create(string, interface{}) (*CrmProperty, error) + Get(string, string) (*CrmProperty, error) + Delete(string, string) error + Update(string, string, interface{}) (*CrmProperty, error) +} + +// CrmPropertiesServiceOp handles communication with the CRM properties endpoint. +type CrmPropertiesServiceOp struct { + client *Client + crmPropertiesPath string +} + +var _ CrmPropertiesService = (*CrmPropertiesServiceOp)(nil) + +func (s *CrmPropertiesServiceOp) List(objectType string) (*CrmPropertiesList, error) { + var resource CrmPropertiesList + path := fmt.Sprintf("%s/%s", s.crmPropertiesPath, objectType) + if err := s.client.Get(path, &resource, nil); err != nil { + return nil, err + } + return &resource, nil +} + +func (s *CrmPropertiesServiceOp) Get(objectType, propertyName string) (*CrmProperty, error) { + var resource CrmProperty + path := fmt.Sprintf("%s/%s/%s", s.crmPropertiesPath, objectType, propertyName) + if err := s.client.Get(path, &resource, nil); err != nil { + return nil, err + } + return &resource, nil +} + +func (s *CrmPropertiesServiceOp) Create(objectType string, reqData interface{}) (*CrmProperty, error) { + var resource CrmProperty + path := fmt.Sprintf("%s/%s", s.crmPropertiesPath, objectType) + if err := s.client.Post(path, reqData, &resource); err != nil { + return nil, err + } + return &resource, nil +} + +func (s *CrmPropertiesServiceOp) Delete(objectType string, propertyName string) error { + path := fmt.Sprintf("%s/%s/%s", s.crmPropertiesPath, objectType, propertyName) + return s.client.Delete(path, nil) +} + +func (s *CrmPropertiesServiceOp) Update(objectType string, propertyName string, reqData interface{}) (*CrmProperty, error) { + var resource CrmProperty + path := fmt.Sprintf("%s/%s/%s", s.crmPropertiesPath, objectType, propertyName) + if err := s.client.Patch(path, reqData, &resource); err != nil { + return nil, err + } + return &resource, nil +} diff --git a/crm_properties_test.go b/crm_properties_test.go new file mode 100644 index 0000000..1910138 --- /dev/null +++ b/crm_properties_test.go @@ -0,0 +1,84 @@ +package hubspot + +import ( + "fmt" + "os" + "testing" + "time" +) + +func TestListCrmProperties(t *testing.T) { + t.SkipNow() + cli, _ := NewClient(SetPrivateAppToken(os.Getenv("PRIVATE_APP_TOKEN"))) + // Use crm_schemas:TestCreate() to generate this... + res, err := cli.CRM.Properties.List("cars") + if err != nil { + t.Error(err) + } + + if len(res.Results) < 1 { + t.Error("expected len(res.Results) to be > 1") + } + +} + +func TestGetCrmProperty(t *testing.T) { + t.SkipNow() + + cli, _ := NewClient(SetPrivateAppToken(os.Getenv("PRIVATE_APP_TOKEN"))) + // Use crm_schemas:TestCreate() to generate this... + res, err := cli.CRM.Properties.Get("cars", "model") + if err != nil { + t.Error(err) + } + + if *res.Name != "model" { + t.Errorf("expected res.Name to be model, got %s", res.Name) + } + +} + +func TestCreateProperty(t *testing.T) { + t.SkipNow() + cli, _ := NewClient(SetPrivateAppToken(os.Getenv("PRIVATE_APP_TOKEN"))) + newProp := &CrmProperty{ + Name: NewString("mileage"), + Label: NewString("Mileage Label"), + Type: NewString("number"), + FieldType: NewString("number"), + GroupName: NewString("cars_information"), + } + + _, err := cli.CRM.Properties.Create("cars", newProp) + if err != nil { + t.Error(err) + return + } +} + +func TestUpdateProperty(t *testing.T) { + t.SkipNow() + cli, _ := NewClient(SetPrivateAppToken(os.Getenv("PRIVATE_APP_TOKEN"))) + + updateProp := make(map[string]interface{}) + updateProp["label"] = fmt.Sprintf("Updated Label %d", time.Now().UnixMicro()) + + res, err := cli.CRM.Properties.Update("cars", "mileage", &updateProp) + if err != nil { + t.Error(err) + return + } + + if res.Label != updateProp["label"] { + t.Errorf("expected res.Label to be %s, got %s", updateProp["label"], res.Label) + } +} + +func TestDeleteProperty(t *testing.T) { + t.SkipNow() + cli, _ := NewClient(SetPrivateAppToken(os.Getenv("PRIVATE_APP_TOKEN"))) + err := cli.CRM.Properties.Delete("cars", "mileage") + if err != nil { + t.Error(err) + } +} diff --git a/crm_schemas.go b/crm_schemas.go new file mode 100644 index 0000000..7171f69 --- /dev/null +++ b/crm_schemas.go @@ -0,0 +1,105 @@ +package hubspot + +import ( + "fmt" +) + +const ( + crmSchemasPath = "schemas" +) + +type CrmSchemaAssociation struct { + Cardinality *HsStr `json:"cardinality"` + CreatedAt *HsTime `json:"createdAt"` + FromObjectTypeId *HsStr `json:"fromObjectTypeId"` + ID *HsStr `json:"id"` + InverseCardinality *HsStr `json:"inverseCardinality"` + Name *HsStr `json:"name"` + ToObjectTypeId *HsStr `json:"toObjectTypeId"` + UpdatedAt *HsTime `json:"updatedAt"` +} + +type CrmSchemaLabels struct { + Plural *HsStr `json:"plural"` + Singular *HsStr `json:"singular"` +} + +type CrmSchemasList struct { + Results []*CrmSchema +} + +type CrmSchema struct { + Archived *HsBool `json:"archived"` + Associations []*CrmSchemaAssociation + CreatedAt *HsTime `json:"createdAt"` + FullyQualifiedName *HsStr `json:"fullyQualifiedName"` + ID *HsStr `json:"id"` + Labels CrmSchemaLabels + MetaType *HsStr `json:"metaType"` + Name *HsStr `json:"name"` + ObjectTypeId *HsStr `json:"objectTypeId"` + PrimaryDisplayProperty *HsStr `json:"primaryDisplayProperty"` + Properties []*CrmProperty `json:"properties"` + RequiredProperties []*HsStr `json:"requiredProperties"` + Restorable *HsBool `json:"restorable"` + SearchableProperties []*HsStr `json:"searchableProperties"` + SecondaryDisplayProperties []*HsStr `json:"secondaryDisplayProperties"` + UpdatedAt *HsTime `json:"updatedAt"` +} + +// CrmSchemasService is an interface of CRM schemas endpoints of the HubSpot API. +// Reference: https://developers.hubspot.com/docs/api/crm/crm-custom-objects +type CrmSchemasService interface { + List() (*CrmSchemasList, error) + Create(interface{}) (*CrmSchema, error) + Get(string) (*CrmSchema, error) + Delete(string, *RequestQueryOption) error + Update(string, interface{}) (*CrmSchema, error) +} + +// CrmSchemasServiceOp handles communication with the CRM schemas endpoint. +type CrmSchemasServiceOp struct { + client *Client + crmSchemasPath string +} + +var _ CrmSchemasService = (*CrmSchemasServiceOp)(nil) + +func (s *CrmSchemasServiceOp) List() (*CrmSchemasList, error) { + var resource CrmSchemasList + if err := s.client.Get(s.crmSchemasPath, &resource, nil); err != nil { + return nil, err + } + return &resource, nil +} + +func (s *CrmSchemasServiceOp) Create(reqData interface{}) (*CrmSchema, error) { + var resource CrmSchema + if err := s.client.Post(s.crmSchemasPath, reqData, &resource); err != nil { + return nil, err + } + return &resource, nil +} + +func (s *CrmSchemasServiceOp) Get(schemaIdentifier string) (*CrmSchema, error) { + var resource CrmSchema + path := fmt.Sprintf("%s/%s", s.crmSchemasPath, schemaIdentifier) + if err := s.client.Get(path, &resource, nil); err != nil { + return nil, err + } + return &resource, nil +} + +func (s *CrmSchemasServiceOp) Delete(schemaIdentifier string, option *RequestQueryOption) error { + path := fmt.Sprintf("%s/%s", s.crmSchemasPath, schemaIdentifier) + return s.client.Delete(path, option) +} + +func (s *CrmSchemasServiceOp) Update(schemaIdentifier string, reqData interface{}) (*CrmSchema, error) { + var resource CrmSchema + path := fmt.Sprintf("%s/%s", s.crmSchemasPath, schemaIdentifier) + if err := s.client.Patch(path, reqData, &resource); err != nil { + return nil, err + } + return &resource, nil +} diff --git a/crm_schemas_test.go b/crm_schemas_test.go new file mode 100644 index 0000000..a1fb0b1 --- /dev/null +++ b/crm_schemas_test.go @@ -0,0 +1,194 @@ +package hubspot + +import ( + "encoding/json" + "os" + "testing" +) + +func TestGetSchemas(t *testing.T) { + t.SkipNow() + + cli, _ := NewClient(SetPrivateAppToken(os.Getenv("PRIVATE_APP_TOKEN"))) + res, err := cli.CRM.Schemas.List() + if err != nil { + t.Error(err) + } + if len(res.Results) < 1 { + t.Errorf("expected results to have some results") + } +} + +func TestCreateSchema(t *testing.T) { + t.SkipNow() + // this example is from Hubspot.. + var exampleJson string = ` + { + "name": "cars", + "labels": { + "singular": "Car", + "plural": "Cars" + }, + "primaryDisplayProperty": "model", + "secondaryDisplayProperties": [ + "make" + ], + "searchableProperties": [ + "year", + "make", + "vin", + "model" + ], + "requiredProperties": [ + "year", + "make", + "vin", + "model" + ], + "properties": [ + { + "name": "condition", + "label": "Condition", + "type": "enumeration", + "fieldType": "select", + "options": [ + { + "label": "New", + "value": "new" + }, + { + "label": "Used", + "value": "used" + } + ] + }, + { + "name": "date_received", + "label": "Date received", + "type": "date", + "fieldType": "date" + }, + { + "name": "year", + "label": "Year", + "type": "number", + "fieldType": "number" + }, + { + "name": "make", + "label": "Make", + "type": "string", + "fieldType": "text" + }, + { + "name": "model", + "label": "Model", + "type": "string", + "fieldType": "text" + }, + { + "name": "vin", + "label": "VIN", + "type": "string", + "hasUniqueValue": true, + "fieldType": "text" + }, + { + "name": "price", + "label": "Price", + "type": "number", + "fieldType": "number" + }, + { + "name": "notes", + "label": "Notes", + "type": "string", + "fieldType": "text" + } + ], + "associatedObjects": [ + "CONTACT" + ] + } + ` + + req := make(map[string]interface{}) + err := json.Unmarshal([]byte(exampleJson), &req) + if err != nil { + t.Error(err) + } + + cli, _ := NewClient(SetPrivateAppToken(os.Getenv("PRIVATE_APP_TOKEN"))) + res, err := cli.CRM.Schemas.Create(req) + if err != nil { + t.Error(err) + } + + if *res.Name != "cars" { + t.Errorf("expected post schema result to have an id field") + } +} + +func TestGetSchema(t *testing.T) { + t.SkipNow() + cli, _ := NewClient(SetPrivateAppToken(os.Getenv("PRIVATE_APP_TOKEN"))) + res, err := cli.CRM.Schemas.Get("cars") + if err != nil { + t.Error(err) + } + if *res.Name != "cars" { + t.Errorf("expected post schema result to have an id field") + } +} + +func TestDeleteSchema(t *testing.T) { + t.SkipNow() + cli, _ := NewClient(SetPrivateAppToken(os.Getenv("PRIVATE_APP_TOKEN"))) + + res, err := cli.CRM.Schemas.Get("cars") + if err != nil { + t.Error(err) + } + + err = cli.CRM.Schemas.Delete(string(*res.FullyQualifiedName), &RequestQueryOption{Archived: false}) + if err != nil { + t.Error(err) + } + + err = cli.CRM.Schemas.Delete(string(*res.FullyQualifiedName), &RequestQueryOption{Archived: true}) + if err != nil { + t.Error(err) + } + +} + +func TestUpdateSchema(t *testing.T) { + t.SkipNow() + // Note: You need to wait some time after calling create before calling Update as it'll return an error message. + req := make(map[string]interface{}) + req["primaryDisplayProperty"] = "year" + + cli, _ := NewClient(SetPrivateAppToken(os.Getenv("PRIVATE_APP_TOKEN"))) + + res, err := cli.CRM.Schemas.Get("cars") + if err != nil { + t.Error(err) + return + } + + if *res.PrimaryDisplayProperty != "model" { + t.Error("expected primaryDisplayProperty to be model before update") + return + } + + res, err = cli.CRM.Schemas.Update(string(*res.FullyQualifiedName), req) + if err != nil { + t.Error(err) + return + } + + if *res.PrimaryDisplayProperty != "year" { + t.Errorf("expected primaryDisplayProperty to be year after update, got %s", res.PrimaryDisplayProperty) + } + +} diff --git a/gohubspot.go b/gohubspot.go index bdddaf7..6d19411 100644 --- a/gohubspot.go +++ b/gohubspot.go @@ -248,8 +248,8 @@ func (c *Client) Patch(path string, data, resource interface{}) error { } // Delete performs a DELETE request for the given path. -func (c *Client) Delete(path string) error { - return c.CreateAndDo(http.MethodDelete, path, "application/json", nil, nil, nil) +func (c *Client) Delete(path string, option interface{}) error { + return c.CreateAndDo(http.MethodDelete, path, "application/json", nil, option, nil) } func (c *Client) PostMultipart(path, boundary string, data, resource interface{}) error { diff --git a/type.go b/type.go index f0c07e4..fda3d7d 100644 --- a/type.go +++ b/type.go @@ -93,3 +93,10 @@ func (ht *HsTime) ToTime() *time.Time { } return &v } + +type HsInt int + +func NewInt(v int) *HsInt { + val := HsInt(v) + return &val +}