@memberjunction/integration-connectors 5.13.0 → 5.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  import { RegisterClass } from '@memberjunction/global';
8
- import { Metadata } from '@memberjunction/core';
8
+ import { Metadata, RunView } from '@memberjunction/core';
9
9
  import { BaseIntegrationConnector, BaseRESTIntegrationConnector, } from '@memberjunction/integration-engine';
10
10
  // ─── Constants ────────────────────────────────────────────────────────
11
11
  const HUBSPOT_API_BASE = 'https://api.hubapi.com';
@@ -17,98 +17,287 @@ const REQUEST_TIMEOUT_MS = 30000;
17
17
  /** Minimum milliseconds between API requests (HubSpot: 100 req/10s for private apps) */
18
18
  const MIN_REQUEST_INTERVAL_MS = 100;
19
19
  /**
20
- * Known properties to request per object type. HubSpot's list endpoint
21
- * only returns a handful of default properties unless you explicitly
22
- * specify which properties to include via the `properties` query param.
20
+ * Comprehensive HubSpot object metadata — single source of truth for both
21
+ * action generation and API property requests.
22
+ *
23
+ * CRM objects (contacts, companies, deals, tasks, tickets, products) generate
24
+ * CRUD actions. Activity/ancillary objects (calls, emails, notes, meetings,
25
+ * line_items, quotes, feedback_submissions) are included for property lookups
26
+ * but excluded from action generation via IncludeInActionGeneration: false.
23
27
  */
24
- const HUBSPOT_PROPERTIES = {
25
- contacts: [
26
- 'email', 'firstname', 'lastname', 'phone', 'mobilephone', 'company',
27
- 'jobtitle', 'lifecyclestage', 'hs_lead_status', 'address', 'city',
28
- 'state', 'zip', 'country', 'website', 'industry',
29
- 'annualrevenue', 'numberofemployees', 'hs_object_id',
30
- 'createdate', 'lastmodifieddate', 'associatedcompanyid',
31
- 'notes_last_contacted', 'notes_last_updated', 'hs_email_optout',
32
- ],
33
- companies: [
34
- 'name', 'domain', 'industry', 'phone', 'address', 'address2',
35
- 'city', 'state', 'zip', 'country', 'website', 'description',
36
- 'numberofemployees', 'annualrevenue', 'lifecyclestage',
37
- 'type', 'hs_object_id', 'createdate', 'hs_lastmodifieddate',
38
- 'founded_year', 'is_public',
39
- ],
40
- deals: [
41
- 'dealname', 'amount', 'dealstage', 'pipeline', 'closedate',
42
- 'createdate', 'hs_object_id', 'hs_lastmodifieddate',
43
- 'dealtype', 'description', 'hs_deal_stage_probability',
44
- 'hs_projected_amount', 'hs_priority', 'hubspot_owner_id',
45
- 'notes_last_contacted', 'num_associated_contacts',
46
- ],
47
- tickets: [
48
- 'subject', 'content', 'hs_pipeline', 'hs_pipeline_stage',
49
- 'hs_ticket_priority', 'hs_ticket_category', 'createdate',
50
- 'hs_lastmodifieddate', 'hs_object_id', 'closed_date',
51
- 'source_type', 'hubspot_owner_id',
52
- ],
53
- products: [
54
- 'name', 'description', 'price', 'hs_cost_of_goods_sold',
55
- 'hs_recurring_billing_period', 'hs_sku', 'tax',
56
- 'createdate', 'hs_lastmodifieddate', 'hs_object_id',
57
- ],
58
- line_items: [
59
- 'name', 'description', 'quantity', 'price', 'amount',
60
- 'discount', 'tax', 'hs_product_id', 'hs_line_item_currency_code',
61
- 'hs_sku', 'hs_cost_of_goods_sold', 'hs_recurring_billing_period',
62
- 'createdate', 'hs_lastmodifieddate', 'hs_object_id',
63
- ],
64
- quotes: [
65
- 'hs_title', 'hs_expiration_date', 'hs_status', 'hs_quote_amount',
66
- 'hs_currency', 'hs_sender_firstname', 'hs_sender_lastname',
67
- 'hs_sender_email', 'hs_sender_company_name', 'hs_language',
68
- 'hs_locale', 'hs_slug', 'hs_public_url_key',
69
- 'createdate', 'hs_lastmodifieddate', 'hs_object_id',
70
- ],
71
- calls: [
72
- 'hs_call_title', 'hs_call_body', 'hs_call_status',
73
- 'hs_call_direction', 'hs_call_duration', 'hs_call_from_number',
74
- 'hs_call_to_number', 'hs_call_disposition',
75
- 'hs_call_recording_url', 'hubspot_owner_id',
76
- 'hs_timestamp', 'createdate', 'hs_lastmodifieddate', 'hs_object_id',
77
- ],
78
- emails: [
79
- 'hs_email_subject', 'hs_email_text', 'hs_email_html',
80
- 'hs_email_status', 'hs_email_direction',
81
- 'hs_email_sender_email', 'hs_email_sender_firstname',
82
- 'hs_email_sender_lastname', 'hs_email_to_email',
83
- 'hubspot_owner_id',
84
- 'hs_timestamp', 'createdate', 'hs_lastmodifieddate', 'hs_object_id',
85
- ],
86
- notes: [
87
- 'hs_note_body', 'hs_timestamp', 'hubspot_owner_id',
88
- 'hs_attachment_ids', 'hs_body_preview', 'hs_body_preview_is_truncated',
89
- 'createdate', 'hs_lastmodifieddate', 'hs_object_id',
90
- ],
91
- tasks: [
92
- 'hs_task_subject', 'hs_task_body', 'hs_task_status',
93
- 'hs_task_priority', 'hs_task_type', 'hs_timestamp',
94
- 'hs_task_completion_date', 'hs_queue_membership_ids',
95
- 'hubspot_owner_id',
96
- 'createdate', 'hs_lastmodifieddate', 'hs_object_id',
97
- ],
98
- meetings: [
99
- 'hs_meeting_title', 'hs_meeting_body', 'hs_meeting_start_time',
100
- 'hs_meeting_end_time', 'hs_meeting_outcome', 'hs_meeting_location',
101
- 'hs_meeting_external_url', 'hs_internal_meeting_notes',
102
- 'hs_activity_type', 'hubspot_owner_id',
103
- 'hs_timestamp', 'createdate', 'hs_lastmodifieddate', 'hs_object_id',
104
- ],
105
- feedback_submissions: [
106
- 'hs_survey_id', 'hs_survey_name', 'hs_survey_type',
107
- 'hs_submission_name', 'hs_content', 'hs_response_group',
108
- 'hs_sentiment', 'hs_survey_channel',
109
- 'hs_timestamp', 'createdate', 'hs_lastmodifieddate', 'hs_object_id',
110
- ],
111
- };
28
+ const HUBSPOT_OBJECTS = [
29
+ // ── CRM Objects (generate actions) ──────────────────────────────────
30
+ {
31
+ Name: 'contacts', DisplayName: 'Contact',
32
+ Description: 'A person or lead in HubSpot CRM', SupportsWrite: true,
33
+ Fields: [
34
+ { Name: 'email', DisplayName: 'Email', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Contact email address' },
35
+ { Name: 'firstname', DisplayName: 'First Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Contact first name' },
36
+ { Name: 'lastname', DisplayName: 'Last Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Contact last name' },
37
+ { Name: 'phone', DisplayName: 'Phone', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Contact phone number' },
38
+ { Name: 'mobilephone', DisplayName: 'Mobile Phone', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Mobile phone number' },
39
+ { Name: 'company', DisplayName: 'Company', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Associated company name' },
40
+ { Name: 'jobtitle', DisplayName: 'Job Title', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Contact job title' },
41
+ { Name: 'lifecyclestage', DisplayName: 'Lifecycle Stage', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Lifecycle stage (subscriber, lead, opportunity, customer, etc.)' },
42
+ { Name: 'hs_lead_status', DisplayName: 'Lead Status', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Lead qualification status' },
43
+ { Name: 'address', DisplayName: 'Address', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Street address' },
44
+ { Name: 'city', DisplayName: 'City', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'City' },
45
+ { Name: 'state', DisplayName: 'State', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'State or province' },
46
+ { Name: 'zip', DisplayName: 'Zip', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Postal/zip code' },
47
+ { Name: 'country', DisplayName: 'Country', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Country' },
48
+ { Name: 'website', DisplayName: 'Website', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Contact website URL' },
49
+ { Name: 'industry', DisplayName: 'Industry', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Industry' },
50
+ { Name: 'annualrevenue', DisplayName: 'Annual Revenue', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Annual revenue' },
51
+ { Name: 'numberofemployees', DisplayName: 'Number of Employees', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Number of employees' },
52
+ { Name: 'associatedcompanyid', DisplayName: 'Associated Company ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'ID of associated company' },
53
+ { Name: 'notes_last_contacted', DisplayName: 'Last Contacted', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last contacted' },
54
+ { Name: 'notes_last_updated', DisplayName: 'Notes Last Updated', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When notes were last updated' },
55
+ { Name: 'hs_email_optout', DisplayName: 'Email Opt-out', Type: 'boolean', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Whether contact has opted out of email' },
56
+ { Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
57
+ { Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When the contact was created' },
58
+ { Name: 'lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
59
+ ],
60
+ },
61
+ {
62
+ Name: 'companies', DisplayName: 'Company',
63
+ Description: 'A business organization in HubSpot CRM', SupportsWrite: true,
64
+ Fields: [
65
+ { Name: 'name', DisplayName: 'Company Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Company name' },
66
+ { Name: 'domain', DisplayName: 'Domain', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Company website domain' },
67
+ { Name: 'industry', DisplayName: 'Industry', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Company industry' },
68
+ { Name: 'phone', DisplayName: 'Phone', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Company phone number' },
69
+ { Name: 'address', DisplayName: 'Address', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Street address' },
70
+ { Name: 'address2', DisplayName: 'Address Line 2', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Address line 2' },
71
+ { Name: 'city', DisplayName: 'City', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'City' },
72
+ { Name: 'state', DisplayName: 'State', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'State or province' },
73
+ { Name: 'zip', DisplayName: 'Zip', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Postal/zip code' },
74
+ { Name: 'country', DisplayName: 'Country', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Country' },
75
+ { Name: 'website', DisplayName: 'Website', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Company website URL' },
76
+ { Name: 'description', DisplayName: 'Description', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Company description' },
77
+ { Name: 'numberofemployees', DisplayName: 'Employees', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Number of employees' },
78
+ { Name: 'annualrevenue', DisplayName: 'Annual Revenue', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Annual revenue' },
79
+ { Name: 'lifecyclestage', DisplayName: 'Lifecycle Stage', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Company lifecycle stage' },
80
+ { Name: 'type', DisplayName: 'Type', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Company type' },
81
+ { Name: 'founded_year', DisplayName: 'Founded Year', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Year the company was founded' },
82
+ { Name: 'is_public', DisplayName: 'Is Public', Type: 'boolean', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Whether the company is publicly traded' },
83
+ { Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
84
+ { Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When the company was created' },
85
+ { Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
86
+ ],
87
+ },
88
+ {
89
+ Name: 'deals', DisplayName: 'Deal',
90
+ Description: 'A sales deal/opportunity in HubSpot CRM', SupportsWrite: true,
91
+ Fields: [
92
+ { Name: 'dealname', DisplayName: 'Deal Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Name of the deal' },
93
+ { Name: 'amount', DisplayName: 'Amount', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Deal value/amount' },
94
+ { Name: 'dealstage', DisplayName: 'Deal Stage', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Current stage in the sales pipeline' },
95
+ { Name: 'pipeline', DisplayName: 'Pipeline', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Sales pipeline' },
96
+ { Name: 'closedate', DisplayName: 'Close Date', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Expected close date' },
97
+ { Name: 'dealtype', DisplayName: 'Deal Type', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Type of deal' },
98
+ { Name: 'description', DisplayName: 'Description', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Deal description' },
99
+ { Name: 'hs_deal_stage_probability', DisplayName: 'Stage Probability', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Probability based on deal stage' },
100
+ { Name: 'hs_projected_amount', DisplayName: 'Projected Amount', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Projected deal amount' },
101
+ { Name: 'hs_priority', DisplayName: 'Priority', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Deal priority level' },
102
+ { Name: 'hubspot_owner_id', DisplayName: 'Owner', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'HubSpot owner user ID' },
103
+ { Name: 'notes_last_contacted', DisplayName: 'Last Contacted', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last contacted' },
104
+ { Name: 'num_associated_contacts', DisplayName: 'Associated Contacts', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Number of associated contacts' },
105
+ { Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
106
+ { Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When the deal was created' },
107
+ { Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
108
+ ],
109
+ },
110
+ {
111
+ Name: 'tasks', DisplayName: 'Task',
112
+ Description: 'A task/to-do item in HubSpot CRM', SupportsWrite: true,
113
+ Fields: [
114
+ { Name: 'hs_task_subject', DisplayName: 'Subject', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Task subject line' },
115
+ { Name: 'hs_task_body', DisplayName: 'Body', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Task body/description' },
116
+ { Name: 'hs_task_status', DisplayName: 'Status', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Task status (NOT_STARTED, IN_PROGRESS, COMPLETED, etc.)' },
117
+ { Name: 'hs_task_priority', DisplayName: 'Priority', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Task priority level' },
118
+ { Name: 'hs_task_type', DisplayName: 'Type', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Task type (TODO, CALL, EMAIL)' },
119
+ { Name: 'hs_timestamp', DisplayName: 'Due Date', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Task due date/timestamp' },
120
+ { Name: 'hs_task_completion_date', DisplayName: 'Completion Date', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'When the task was completed' },
121
+ { Name: 'hs_queue_membership_ids', DisplayName: 'Queue IDs', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Task queue membership IDs' },
122
+ { Name: 'hubspot_owner_id', DisplayName: 'Owner', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'HubSpot owner user ID' },
123
+ { Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
124
+ { Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When the task was created' },
125
+ { Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
126
+ ],
127
+ },
128
+ {
129
+ Name: 'tickets', DisplayName: 'Ticket',
130
+ Description: 'A support ticket in HubSpot CRM', SupportsWrite: true,
131
+ Fields: [
132
+ { Name: 'subject', DisplayName: 'Subject', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Ticket subject line' },
133
+ { Name: 'content', DisplayName: 'Content', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Ticket body/content' },
134
+ { Name: 'hs_pipeline', DisplayName: 'Pipeline', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Support pipeline' },
135
+ { Name: 'hs_pipeline_stage', DisplayName: 'Pipeline Stage', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Current pipeline stage' },
136
+ { Name: 'hs_ticket_priority', DisplayName: 'Priority', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Ticket priority level' },
137
+ { Name: 'hs_ticket_category', DisplayName: 'Category', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Ticket category' },
138
+ { Name: 'source_type', DisplayName: 'Source', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'How the ticket was created' },
139
+ { Name: 'hubspot_owner_id', DisplayName: 'Owner', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'HubSpot owner user ID' },
140
+ { Name: 'closed_date', DisplayName: 'Closed Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When the ticket was closed' },
141
+ { Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
142
+ { Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When the ticket was created' },
143
+ { Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
144
+ ],
145
+ },
146
+ {
147
+ Name: 'products', DisplayName: 'Product',
148
+ Description: 'A product in the HubSpot product catalog', SupportsWrite: true,
149
+ Fields: [
150
+ { Name: 'name', DisplayName: 'Product Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Product name' },
151
+ { Name: 'description', DisplayName: 'Description', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Product description' },
152
+ { Name: 'price', DisplayName: 'Price', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Product unit price' },
153
+ { Name: 'hs_cost_of_goods_sold', DisplayName: 'Cost of Goods Sold', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Cost of goods sold' },
154
+ { Name: 'hs_recurring_billing_period', DisplayName: 'Recurring Billing Period', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Recurring billing period' },
155
+ { Name: 'hs_sku', DisplayName: 'SKU', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Stock keeping unit identifier' },
156
+ { Name: 'tax', DisplayName: 'Tax', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Tax amount' },
157
+ { Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
158
+ { Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When the product was created' },
159
+ { Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
160
+ ],
161
+ },
162
+ // ── Ancillary Objects (property lookups only, no action generation) ──
163
+ {
164
+ Name: 'line_items', DisplayName: 'Line Item',
165
+ Description: 'A line item on a deal or quote', SupportsWrite: false, IncludeInActionGeneration: false,
166
+ Fields: [
167
+ { Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Line item name' },
168
+ { Name: 'description', DisplayName: 'Description', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Line item description' },
169
+ { Name: 'quantity', DisplayName: 'Quantity', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Quantity' },
170
+ { Name: 'price', DisplayName: 'Price', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Unit price' },
171
+ { Name: 'amount', DisplayName: 'Amount', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Total amount' },
172
+ { Name: 'discount', DisplayName: 'Discount', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Discount amount' },
173
+ { Name: 'tax', DisplayName: 'Tax', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Tax amount' },
174
+ { Name: 'hs_product_id', DisplayName: 'Product ID', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Associated product ID' },
175
+ { Name: 'hs_line_item_currency_code', DisplayName: 'Currency', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Currency code' },
176
+ { Name: 'hs_sku', DisplayName: 'SKU', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Stock keeping unit' },
177
+ { Name: 'hs_cost_of_goods_sold', DisplayName: 'Cost of Goods Sold', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Cost of goods sold' },
178
+ { Name: 'hs_recurring_billing_period', DisplayName: 'Recurring Billing Period', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Recurring billing period' },
179
+ { Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
180
+ { Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
181
+ { Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
182
+ ],
183
+ },
184
+ {
185
+ Name: 'quotes', DisplayName: 'Quote',
186
+ Description: 'A sales quote in HubSpot', SupportsWrite: false, IncludeInActionGeneration: false,
187
+ Fields: [
188
+ { Name: 'hs_title', DisplayName: 'Title', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Quote title' },
189
+ { Name: 'hs_expiration_date', DisplayName: 'Expiration Date', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Quote expiration date' },
190
+ { Name: 'hs_status', DisplayName: 'Status', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Quote status' },
191
+ { Name: 'hs_quote_amount', DisplayName: 'Amount', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Quote total amount' },
192
+ { Name: 'hs_currency', DisplayName: 'Currency', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Currency code' },
193
+ { Name: 'hs_sender_firstname', DisplayName: 'Sender First Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Sender first name' },
194
+ { Name: 'hs_sender_lastname', DisplayName: 'Sender Last Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Sender last name' },
195
+ { Name: 'hs_sender_email', DisplayName: 'Sender Email', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Sender email' },
196
+ { Name: 'hs_sender_company_name', DisplayName: 'Sender Company', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Sender company name' },
197
+ { Name: 'hs_language', DisplayName: 'Language', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Quote language' },
198
+ { Name: 'hs_locale', DisplayName: 'Locale', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Quote locale' },
199
+ { Name: 'hs_slug', DisplayName: 'Slug', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'URL slug' },
200
+ { Name: 'hs_public_url_key', DisplayName: 'Public URL Key', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Public URL access key' },
201
+ { Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
202
+ { Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
203
+ { Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
204
+ ],
205
+ },
206
+ // ── Activity Objects (property lookups only, no action generation) ───
207
+ {
208
+ Name: 'calls', DisplayName: 'Call',
209
+ Description: 'A call activity in HubSpot', SupportsWrite: false, IncludeInActionGeneration: false,
210
+ Fields: [
211
+ { Name: 'hs_call_title', DisplayName: 'Title', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Call title' },
212
+ { Name: 'hs_call_body', DisplayName: 'Body', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Call notes/body' },
213
+ { Name: 'hs_call_status', DisplayName: 'Status', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Call status' },
214
+ { Name: 'hs_call_direction', DisplayName: 'Direction', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Inbound or outbound' },
215
+ { Name: 'hs_call_duration', DisplayName: 'Duration', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Call duration in ms' },
216
+ { Name: 'hs_call_from_number', DisplayName: 'From Number', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Caller phone number' },
217
+ { Name: 'hs_call_to_number', DisplayName: 'To Number', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Recipient phone number' },
218
+ { Name: 'hs_call_disposition', DisplayName: 'Disposition', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Call outcome disposition' },
219
+ { Name: 'hs_call_recording_url', DisplayName: 'Recording URL', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Call recording URL' },
220
+ { Name: 'hubspot_owner_id', DisplayName: 'Owner', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'HubSpot owner user ID' },
221
+ { Name: 'hs_timestamp', DisplayName: 'Timestamp', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Call timestamp' },
222
+ { Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
223
+ { Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
224
+ { Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
225
+ ],
226
+ },
227
+ {
228
+ Name: 'emails', DisplayName: 'Email',
229
+ Description: 'An email activity in HubSpot', SupportsWrite: false, IncludeInActionGeneration: false,
230
+ Fields: [
231
+ { Name: 'hs_email_subject', DisplayName: 'Subject', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Email subject' },
232
+ { Name: 'hs_email_text', DisplayName: 'Text Body', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Plain text body' },
233
+ { Name: 'hs_email_html', DisplayName: 'HTML Body', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'HTML body' },
234
+ { Name: 'hs_email_status', DisplayName: 'Status', Type: 'enum', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Email status' },
235
+ { Name: 'hs_email_direction', DisplayName: 'Direction', Type: 'enum', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Inbound or outbound' },
236
+ { Name: 'hs_email_sender_email', DisplayName: 'Sender Email', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Sender email address' },
237
+ { Name: 'hs_email_sender_firstname', DisplayName: 'Sender First Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Sender first name' },
238
+ { Name: 'hs_email_sender_lastname', DisplayName: 'Sender Last Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Sender last name' },
239
+ { Name: 'hs_email_to_email', DisplayName: 'To Email', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Recipient email address' },
240
+ { Name: 'hubspot_owner_id', DisplayName: 'Owner', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'HubSpot owner user ID' },
241
+ { Name: 'hs_timestamp', DisplayName: 'Timestamp', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Email timestamp' },
242
+ { Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
243
+ { Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
244
+ { Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
245
+ ],
246
+ },
247
+ {
248
+ Name: 'notes', DisplayName: 'Note',
249
+ Description: 'A note activity in HubSpot', SupportsWrite: false, IncludeInActionGeneration: false,
250
+ Fields: [
251
+ { Name: 'hs_note_body', DisplayName: 'Body', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Note body content' },
252
+ { Name: 'hs_timestamp', DisplayName: 'Timestamp', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Note timestamp' },
253
+ { Name: 'hubspot_owner_id', DisplayName: 'Owner', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'HubSpot owner user ID' },
254
+ { Name: 'hs_attachment_ids', DisplayName: 'Attachment IDs', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Attached file IDs' },
255
+ { Name: 'hs_body_preview', DisplayName: 'Body Preview', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Truncated body preview' },
256
+ { Name: 'hs_body_preview_is_truncated', DisplayName: 'Preview Truncated', Type: 'boolean', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Whether preview is truncated' },
257
+ { Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
258
+ { Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
259
+ { Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
260
+ ],
261
+ },
262
+ {
263
+ Name: 'meetings', DisplayName: 'Meeting',
264
+ Description: 'A meeting activity in HubSpot', SupportsWrite: false, IncludeInActionGeneration: false,
265
+ Fields: [
266
+ { Name: 'hs_meeting_title', DisplayName: 'Title', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Meeting title' },
267
+ { Name: 'hs_meeting_body', DisplayName: 'Body', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Meeting description' },
268
+ { Name: 'hs_meeting_start_time', DisplayName: 'Start Time', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Meeting start time' },
269
+ { Name: 'hs_meeting_end_time', DisplayName: 'End Time', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Meeting end time' },
270
+ { Name: 'hs_meeting_outcome', DisplayName: 'Outcome', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Meeting outcome' },
271
+ { Name: 'hs_meeting_location', DisplayName: 'Location', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Meeting location' },
272
+ { Name: 'hs_meeting_external_url', DisplayName: 'External URL', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'External meeting URL' },
273
+ { Name: 'hs_internal_meeting_notes', DisplayName: 'Internal Notes', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Internal meeting notes' },
274
+ { Name: 'hs_activity_type', DisplayName: 'Activity Type', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Activity type' },
275
+ { Name: 'hubspot_owner_id', DisplayName: 'Owner', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'HubSpot owner user ID' },
276
+ { Name: 'hs_timestamp', DisplayName: 'Timestamp', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Meeting timestamp' },
277
+ { Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
278
+ { Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
279
+ { Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
280
+ ],
281
+ },
282
+ {
283
+ Name: 'feedback_submissions', DisplayName: 'Feedback Submission',
284
+ Description: 'A feedback survey submission in HubSpot', SupportsWrite: false, IncludeInActionGeneration: false,
285
+ Fields: [
286
+ { Name: 'hs_survey_id', DisplayName: 'Survey ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Survey ID' },
287
+ { Name: 'hs_survey_name', DisplayName: 'Survey Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Survey name' },
288
+ { Name: 'hs_survey_type', DisplayName: 'Survey Type', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Type of survey' },
289
+ { Name: 'hs_submission_name', DisplayName: 'Submission Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Submission name' },
290
+ { Name: 'hs_content', DisplayName: 'Content', Type: 'text', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Submission content' },
291
+ { Name: 'hs_response_group', DisplayName: 'Response Group', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Response group' },
292
+ { Name: 'hs_sentiment', DisplayName: 'Sentiment', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Response sentiment' },
293
+ { Name: 'hs_survey_channel', DisplayName: 'Channel', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Survey channel' },
294
+ { Name: 'hs_timestamp', DisplayName: 'Timestamp', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Submission timestamp' },
295
+ { Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
296
+ { Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
297
+ { Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
298
+ ],
299
+ },
300
+ ];
112
301
  // ─── Connector ────────────────────────────────────────────────────────
113
302
  /**
114
303
  * Connector for HubSpot CRM via the HubSpot REST API v3.
@@ -127,7 +316,8 @@ const HUBSPOT_PROPERTIES = {
127
316
  * "MinRequestIntervalMs": 100 // optional, default: 100
128
317
  * }
129
318
  *
130
- * DATA SAFETY: This connector is READ-ONLY. It never writes data back to HubSpot.
319
+ * Supports full CRUD: Get, Create, Update, Delete, Search, and List operations
320
+ * on all HubSpot CRM object types.
131
321
  */
132
322
  let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnector {
133
323
  constructor() {
@@ -141,6 +331,195 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
141
331
  get effectiveMaxRetries() { return this._config?.MaxRetries ?? MAX_RETRIES; }
142
332
  get effectiveRequestTimeoutMs() { return this._config?.RequestTimeoutMs ?? REQUEST_TIMEOUT_MS; }
143
333
  get effectiveMinRequestIntervalMs() { return this._config?.MinRequestIntervalMs ?? MIN_REQUEST_INTERVAL_MS; }
334
+ // ─── Capability Getters ──────────────────────────────────────────────
335
+ get SupportsCreate() { return true; }
336
+ get SupportsUpdate() { return true; }
337
+ get SupportsDelete() { return true; }
338
+ get SupportsSearch() { return true; }
339
+ get SupportsListing() { return true; }
340
+ get IntegrationName() { return 'HubSpot'; }
341
+ // ─── Action Metadata ─────────────────────────────────────────────────
342
+ GetIntegrationObjects() {
343
+ return HUBSPOT_OBJECTS;
344
+ }
345
+ GetActionGeneratorConfig() {
346
+ const config = super.GetActionGeneratorConfig();
347
+ if (!config)
348
+ return null;
349
+ config.IconClass = 'fa-brands fa-hubspot';
350
+ config.CreateCategory = false; // HubSpot category already exists in metadata/action-categories
351
+ return config;
352
+ }
353
+ // ─── CRUD Operations ─────────────────────────────────────────────────
354
+ /**
355
+ * Retrieves a single record by ExternalID (HubSpot object ID).
356
+ */
357
+ async GetRecord(ctx) {
358
+ const companyIntegration = ctx.CompanyIntegration;
359
+ const contextUser = ctx.ContextUser;
360
+ const auth = await this.Authenticate(companyIntegration, contextUser);
361
+ const headers = this.BuildHeaders(auth);
362
+ const propertiesParam = this.BuildPropertiesParam(ctx.ObjectName);
363
+ const url = `${HUBSPOT_API_BASE}/crm/v3/objects/${ctx.ObjectName}/${ctx.ExternalID}?${propertiesParam.replace(/^&/, '')}`;
364
+ const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
365
+ if (response.Status === 404)
366
+ return null;
367
+ this.ValidateCRUDResponse(response, 'GetRecord', ctx.ObjectName);
368
+ const raw = response.Body;
369
+ return this.RawToExternalRecord(raw, ctx.ObjectName);
370
+ }
371
+ /**
372
+ * Creates a new record in HubSpot.
373
+ */
374
+ async CreateRecord(ctx) {
375
+ const companyIntegration = ctx.CompanyIntegration;
376
+ const contextUser = ctx.ContextUser;
377
+ const auth = await this.Authenticate(companyIntegration, contextUser);
378
+ const headers = this.BuildHeaders(auth);
379
+ const url = `${HUBSPOT_API_BASE}/crm/v3/objects/${ctx.ObjectName}`;
380
+ const body = { properties: ctx.Attributes };
381
+ const response = await this.MakeHTTPRequest(auth, url, 'POST', headers, body);
382
+ if (response.Status >= 200 && response.Status < 300) {
383
+ const created = response.Body;
384
+ return {
385
+ Success: true,
386
+ ExternalID: String(created['id'] ?? ''),
387
+ StatusCode: response.Status,
388
+ };
389
+ }
390
+ return this.BuildCRUDErrorResult(response, 'CreateRecord', ctx.ObjectName);
391
+ }
392
+ /**
393
+ * Updates an existing record in HubSpot by ExternalID.
394
+ */
395
+ async UpdateRecord(ctx) {
396
+ const companyIntegration = ctx.CompanyIntegration;
397
+ const contextUser = ctx.ContextUser;
398
+ const auth = await this.Authenticate(companyIntegration, contextUser);
399
+ const headers = this.BuildHeaders(auth);
400
+ const url = `${HUBSPOT_API_BASE}/crm/v3/objects/${ctx.ObjectName}/${ctx.ExternalID}`;
401
+ const body = { properties: ctx.Attributes };
402
+ const response = await this.MakeHTTPRequest(auth, url, 'PATCH', headers, body);
403
+ if (response.Status >= 200 && response.Status < 300) {
404
+ const updated = response.Body;
405
+ return {
406
+ Success: true,
407
+ ExternalID: String(updated['id'] ?? ctx.ExternalID),
408
+ StatusCode: response.Status,
409
+ };
410
+ }
411
+ return this.BuildCRUDErrorResult(response, 'UpdateRecord', ctx.ObjectName);
412
+ }
413
+ /**
414
+ * Deletes (archives) a record in HubSpot by ExternalID.
415
+ */
416
+ async DeleteRecord(ctx) {
417
+ const companyIntegration = ctx.CompanyIntegration;
418
+ const contextUser = ctx.ContextUser;
419
+ const auth = await this.Authenticate(companyIntegration, contextUser);
420
+ const headers = this.BuildHeaders(auth);
421
+ const url = `${HUBSPOT_API_BASE}/crm/v3/objects/${ctx.ObjectName}/${ctx.ExternalID}`;
422
+ const response = await this.MakeHTTPRequest(auth, url, 'DELETE', headers);
423
+ if (response.Status === 204 || (response.Status >= 200 && response.Status < 300)) {
424
+ return {
425
+ Success: true,
426
+ ExternalID: ctx.ExternalID,
427
+ StatusCode: response.Status,
428
+ };
429
+ }
430
+ return this.BuildCRUDErrorResult(response, 'DeleteRecord', ctx.ObjectName);
431
+ }
432
+ /**
433
+ * Searches HubSpot objects using the CRM search API.
434
+ */
435
+ async SearchRecords(ctx) {
436
+ const companyIntegration = ctx.CompanyIntegration;
437
+ const contextUser = ctx.ContextUser;
438
+ const auth = await this.Authenticate(companyIntegration, contextUser);
439
+ const headers = this.BuildHeaders(auth);
440
+ const url = `${HUBSPOT_API_BASE}/crm/v3/objects/${ctx.ObjectName}/search`;
441
+ const filters = Object.entries(ctx.Filters).map(([propertyName, value]) => ({
442
+ propertyName,
443
+ operator: 'EQ',
444
+ value,
445
+ }));
446
+ const properties = this.GetObjectFieldNames(ctx.ObjectName);
447
+ const body = {
448
+ filterGroups: [{ filters }],
449
+ properties,
450
+ limit: ctx.PageSize ?? 100,
451
+ after: ctx.Page != null && ctx.Page > 1 ? String((ctx.Page - 1) * (ctx.PageSize ?? 100)) : undefined,
452
+ };
453
+ const response = await this.MakeHTTPRequest(auth, url, 'POST', headers, body);
454
+ this.ValidateCRUDResponse(response, 'SearchRecords', ctx.ObjectName);
455
+ const responseBody = response.Body;
456
+ const results = responseBody.results ?? [];
457
+ const records = results.map(r => this.RawToExternalRecord(r, ctx.ObjectName));
458
+ return {
459
+ Records: records,
460
+ TotalCount: responseBody.total ?? records.length,
461
+ HasMore: responseBody.paging?.next?.after != null,
462
+ };
463
+ }
464
+ /**
465
+ * Lists records from a HubSpot object with cursor-based pagination.
466
+ */
467
+ async ListRecords(ctx) {
468
+ const companyIntegration = ctx.CompanyIntegration;
469
+ const contextUser = ctx.ContextUser;
470
+ const auth = await this.Authenticate(companyIntegration, contextUser);
471
+ const headers = this.BuildHeaders(auth);
472
+ const pageSize = ctx.PageSize ?? 100;
473
+ const propertiesParam = this.BuildPropertiesParam(ctx.ObjectName);
474
+ let url = `${HUBSPOT_API_BASE}/crm/v3/objects/${ctx.ObjectName}?limit=${pageSize}${propertiesParam}`;
475
+ if (ctx.Cursor) {
476
+ url += `&after=${encodeURIComponent(ctx.Cursor)}`;
477
+ }
478
+ const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
479
+ this.ValidateCRUDResponse(response, 'ListRecords', ctx.ObjectName);
480
+ const responseBody = response.Body;
481
+ const results = responseBody.results ?? [];
482
+ const records = results.map(r => this.RawToExternalRecord(r, ctx.ObjectName));
483
+ const nextCursor = responseBody.paging?.next?.after;
484
+ return {
485
+ Records: records,
486
+ HasMore: nextCursor != null,
487
+ NextCursor: nextCursor ?? undefined,
488
+ TotalCount: responseBody.total,
489
+ };
490
+ }
491
+ // ─── CRUD Helpers ────────────────────────────────────────────────────
492
+ /** Converts a raw HubSpot API object to an ExternalRecord. */
493
+ RawToExternalRecord(raw, objectType) {
494
+ const flat = this.FlattenHubSpotRecord(raw);
495
+ return {
496
+ ExternalID: String(raw['id'] ?? ''),
497
+ ObjectType: objectType,
498
+ Fields: flat,
499
+ ModifiedAt: raw['updatedAt'] ? new Date(raw['updatedAt']) : undefined,
500
+ };
501
+ }
502
+ /** Validates a CRUD response and throws on non-2xx status. */
503
+ ValidateCRUDResponse(response, operation, objectName) {
504
+ if (response.Status < 200 || response.Status >= 300) {
505
+ const bodyPreview = typeof response.Body === 'string'
506
+ ? response.Body.slice(0, 500)
507
+ : JSON.stringify(response.Body).slice(0, 500);
508
+ throw new Error(`[HubSpot] ${operation} on ${objectName} failed (HTTP ${response.Status}): ${bodyPreview}`);
509
+ }
510
+ }
511
+ /** Builds a CRUDResult for error responses. */
512
+ BuildCRUDErrorResult(response, operation, objectName) {
513
+ const bodyObj = response.Body;
514
+ const message = bodyObj?.['message']
515
+ ? String(bodyObj['message'])
516
+ : `[HubSpot] ${operation} on ${objectName} failed (HTTP ${response.Status})`;
517
+ return {
518
+ Success: false,
519
+ ErrorMessage: message,
520
+ StatusCode: response.Status,
521
+ };
522
+ }
144
523
  // ─── Abstract method implementations (BaseRESTIntegrationConnector) ──
145
524
  async Authenticate(companyIntegration, contextUser) {
146
525
  console.log(`[HubSpot] Authenticating...`);
@@ -161,7 +540,7 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
161
540
  'Accept': 'application/json',
162
541
  };
163
542
  }
164
- async MakeHTTPRequest(_auth, url, method, headers, _body) {
543
+ async MakeHTTPRequest(_auth, url, method, headers, body) {
165
544
  // Throttle: ensure minimum interval between requests
166
545
  const minInterval = this.effectiveMinRequestIntervalMs;
167
546
  const elapsed = Date.now() - this.lastRequestTime;
@@ -170,7 +549,7 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
170
549
  }
171
550
  const maxRetries = this.effectiveMaxRetries;
172
551
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
173
- const response = await this.FetchWithTimeout(url, method, headers);
552
+ const response = await this.FetchWithTimeout(url, method, headers, body);
174
553
  this.lastRequestTime = Date.now();
175
554
  if (response.status === 429) {
176
555
  const delayMs = this.CalculateRetryDelay(response, attempt);
@@ -179,8 +558,12 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
179
558
  await this.Sleep(delayMs);
180
559
  continue;
181
560
  }
182
- const body = await response.json();
183
- return this.BuildRESTResponse(response, body);
561
+ // Handle empty responses (e.g., 204 No Content from DELETE)
562
+ if (response.status === 204) {
563
+ return this.BuildRESTResponse(response, {});
564
+ }
565
+ const responseBody = await response.json();
566
+ return this.BuildRESTResponse(response, responseBody);
184
567
  }
185
568
  throw new Error(`HubSpot API request failed after ${maxRetries} retries: ${url}`);
186
569
  }
@@ -428,10 +811,12 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
428
811
  FlattenHubSpotRecord(record) {
429
812
  const properties = record['properties'];
430
813
  const result = {};
431
- // Add flattened properties first
814
+ // Add flattened properties — HubSpot uses '' to mean "no value" for all
815
+ // property types, which causes SQL errors on datetime/numeric columns.
816
+ // Normalize empty strings to null so nullable DB columns receive NULL.
432
817
  if (properties) {
433
818
  for (const [key, value] of Object.entries(properties)) {
434
- result[key] = value;
819
+ result[key] = value === '' ? null : value;
435
820
  }
436
821
  }
437
822
  // Add system fields (these override any conflicting property names)
@@ -441,6 +826,153 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
441
826
  result['archived'] = record['archived'];
442
827
  return result;
443
828
  }
829
+ // ─── Association fetch (v4 API) ───────────────────────────────────
830
+ /**
831
+ * Overrides FetchChanges to intercept association objects (Category === 'Association')
832
+ * and route them through the v4 per-object associations endpoint instead of the
833
+ * non-existent flat list endpoint stored in APIPath.
834
+ */
835
+ async FetchChanges(ctx) {
836
+ const obj = this.GetCachedObject(ctx.CompanyIntegration.IntegrationID, ctx.ObjectName);
837
+ if (obj.Category === 'Association') {
838
+ return this.FetchAssociationChanges(ctx, obj);
839
+ }
840
+ return super.FetchChanges(ctx);
841
+ }
842
+ /**
843
+ * Fetches association records in batches by iterating over synced parent (from-side)
844
+ * objects and calling HubSpot's v4 per-object associations endpoint.
845
+ *
846
+ * Uses ctx.CurrentOffset to track parent position across batch calls, so the engine
847
+ * can page through all parents without truncating records.
848
+ */
849
+ async FetchAssociationChanges(ctx, obj) {
850
+ const parsed = this.ParseAssociationPath(obj.APIPath);
851
+ if (!parsed) {
852
+ console.warn(`[HubSpot] Cannot parse association path: ${obj.APIPath}`);
853
+ return { Records: [], HasMore: false };
854
+ }
855
+ const { fromType, toType } = parsed;
856
+ const fields = this.GetCachedFields(obj.ID);
857
+ const auth = await this.Authenticate(ctx.CompanyIntegration, ctx.ContextUser);
858
+ const baseURL = this.GetBaseURL(ctx.CompanyIntegration, auth);
859
+ // Association objects have two PK fields: left (from-side) and right (to-side)
860
+ const pkFields = fields.filter(f => f.IsPrimaryKey).sort((a, b) => a.Sequence - b.Sequence);
861
+ const leftFieldName = pkFields[0]?.Name ?? `${fromType.replace(/s$/, '')}_id`;
862
+ const rightFieldName = pkFields[1]?.Name ?? `${toType.replace(/s$/, '')}_id`;
863
+ const parentIDs = await this.LoadAssociationParentIDs(fromType, ctx);
864
+ const parentOffset = ctx.CurrentOffset ?? 0;
865
+ if (parentOffset === 0) {
866
+ console.log(`[HubSpot] Fetching ${obj.Name}: ${parentIDs.length} parent ${fromType} to iterate`);
867
+ }
868
+ const batchSize = Math.min(ctx.BatchSize ?? 200, 200);
869
+ const batchRecords = [];
870
+ let parentIndex = parentOffset;
871
+ while (parentIndex < parentIDs.length && batchRecords.length < batchSize) {
872
+ const parentID = parentIDs[parentIndex];
873
+ const basePath = `${baseURL}/crm/v4/objects/${fromType}/${parentID}/associations/${toType}`;
874
+ const rawRecords = await this.FetchAllAssociationPages(auth, basePath, obj.DefaultPageSize ?? 500);
875
+ let addedAll = true;
876
+ for (const record of rawRecords) {
877
+ if (batchRecords.length >= batchSize) {
878
+ addedAll = false;
879
+ break;
880
+ }
881
+ const flat = this.FlattenAssociationRecord(record, leftFieldName, parentID, rightFieldName);
882
+ batchRecords.push({
883
+ ExternalID: `${flat[leftFieldName]}|${flat[rightFieldName]}`,
884
+ ObjectType: ctx.ObjectName,
885
+ Fields: flat,
886
+ });
887
+ }
888
+ if (addedAll) {
889
+ parentIndex++;
890
+ }
891
+ else {
892
+ // Don't advance — next batch re-fetches this parent so no records are lost
893
+ break;
894
+ }
895
+ }
896
+ const hasMore = parentIndex < parentIDs.length;
897
+ console.log(`[HubSpot] ${obj.Name}: fetched ${batchRecords.length} association records (parents ${parentOffset}-${parentIndex - 1} of ${parentIDs.length})`);
898
+ return { Records: batchRecords, HasMore: hasMore, NextOffset: hasMore ? parentIndex : undefined };
899
+ }
900
+ /**
901
+ * Paginates through all pages of a HubSpot v4 per-object association endpoint,
902
+ * returning the raw result items (each with toObjectId + associationTypes).
903
+ */
904
+ async FetchAllAssociationPages(auth, basePath, pageSize) {
905
+ const headers = this.BuildHeaders(auth);
906
+ const allRecords = [];
907
+ let cursor;
908
+ do {
909
+ const url = cursor
910
+ ? `${basePath}?limit=${pageSize}&after=${encodeURIComponent(cursor)}`
911
+ : `${basePath}?limit=${pageSize}`;
912
+ const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
913
+ if (response.Status < 200 || response.Status >= 300)
914
+ break;
915
+ const body = response.Body;
916
+ const results = body['results'];
917
+ if (!results || !Array.isArray(results) || results.length === 0)
918
+ break;
919
+ allRecords.push(...results);
920
+ const paging = body['paging'];
921
+ cursor = paging?.next?.after;
922
+ } while (cursor);
923
+ return allRecords;
924
+ }
925
+ /**
926
+ * Converts a HubSpot v4 association result item into a flat record suitable for storage.
927
+ * v4 format: { toObjectId: number, associationTypes: [{ label, typeId, category }] }
928
+ */
929
+ FlattenAssociationRecord(record, leftFieldName, leftValue, rightFieldName) {
930
+ const assocTypes = record['associationTypes'];
931
+ return {
932
+ [leftFieldName]: leftValue,
933
+ [rightFieldName]: String(record['toObjectId']),
934
+ association_type: assocTypes?.[0]?.label ?? null,
935
+ };
936
+ }
937
+ /**
938
+ * Parses a v4 associations APIPath to extract from/to object type names.
939
+ * E.g., "/crm/v4/associations/contacts/companies" → { fromType: "contacts", toType: "companies" }
940
+ */
941
+ ParseAssociationPath(apiPath) {
942
+ const match = /\/crm\/v4\/associations\/([^/?]+)\/([^/?]+)/.exec(apiPath);
943
+ if (!match)
944
+ return null;
945
+ return { fromType: match[1], toType: match[2] };
946
+ }
947
+ /**
948
+ * Loads hs_object_id values for all synced records of a given HubSpot object type
949
+ * by finding its entity map and querying the local MJ entity.
950
+ */
951
+ async LoadAssociationParentIDs(fromType, ctx) {
952
+ const rv = new RunView();
953
+ const entityMapResult = await rv.RunView({
954
+ EntityName: 'MJ: Company Integration Entity Maps',
955
+ ExtraFilter: `ExternalObjectName='${fromType}' AND SyncEnabled=1 AND CompanyIntegrationID='${ctx.CompanyIntegration.ID}'`,
956
+ Fields: ['Entity'],
957
+ MaxRows: 1,
958
+ ResultType: 'simple',
959
+ }, ctx.ContextUser);
960
+ if (!entityMapResult.Success || entityMapResult.Results.length === 0) {
961
+ console.warn(`[HubSpot] No entity map found for ${fromType} — skipping association fetch`);
962
+ return [];
963
+ }
964
+ const entityName = entityMapResult.Results[0].Entity;
965
+ const idsResult = await rv.RunView({
966
+ EntityName: entityName,
967
+ Fields: ['hs_object_id'],
968
+ ResultType: 'simple',
969
+ }, ctx.ContextUser);
970
+ if (!idsResult.Success)
971
+ return [];
972
+ return idsResult.Results
973
+ .map(r => String(r['hs_object_id']))
974
+ .filter(id => id && id !== 'undefined' && id !== 'null');
975
+ }
444
976
  // ─── URL helpers ─────────────────────────────────────────────────
445
977
  /**
446
978
  * Extracts the HubSpot object name from an API path.
@@ -453,25 +985,38 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
453
985
  const segments = pathOnly.split('/').filter(s => s.length > 0);
454
986
  return segments[segments.length - 1] ?? '';
455
987
  }
988
+ /**
989
+ * Returns the known field names for a HubSpot object type, derived from
990
+ * the HUBSPOT_OBJECTS metadata (single source of truth).
991
+ */
992
+ GetObjectFieldNames(objectName) {
993
+ const obj = HUBSPOT_OBJECTS.find(o => o.Name === objectName);
994
+ return obj ? obj.Fields.map(f => f.Name) : [];
995
+ }
456
996
  /**
457
997
  * Builds the `properties` query parameter for a HubSpot object type.
458
998
  * Returns empty string if no properties are configured for the object.
459
999
  */
460
1000
  BuildPropertiesParam(objectName) {
461
- const properties = HUBSPOT_PROPERTIES[objectName];
462
- if (properties && properties.length > 0) {
1001
+ const properties = this.GetObjectFieldNames(objectName);
1002
+ if (properties.length > 0) {
463
1003
  return `&properties=${properties.join(',')}`;
464
1004
  }
465
1005
  return '';
466
1006
  }
467
1007
  // ─── HTTP helpers ────────────────────────────────────────────────
468
- /** Executes an HTTP request with a timeout. */
469
- async FetchWithTimeout(url, method, headers) {
1008
+ /** Executes an HTTP request with a timeout and optional JSON body. */
1009
+ async FetchWithTimeout(url, method, headers, body) {
470
1010
  const controller = new AbortController();
471
1011
  const timeoutMs = this.effectiveRequestTimeoutMs;
472
1012
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
473
1013
  try {
474
- return await fetch(url, { method, headers, signal: controller.signal });
1014
+ const requestInit = { method, headers, signal: controller.signal };
1015
+ if (body !== undefined) {
1016
+ requestInit.body = JSON.stringify(body);
1017
+ requestInit.headers['Content-Type'] = 'application/json';
1018
+ }
1019
+ return await fetch(url, requestInit);
475
1020
  }
476
1021
  catch (err) {
477
1022
  if (err instanceof Error && err.name === 'AbortError') {