@memberjunction/integration-connectors 5.27.1 → 5.28.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.
- package/dist/HubSpotConnector.d.ts +158 -7
- package/dist/HubSpotConnector.d.ts.map +1 -1
- package/dist/HubSpotConnector.js +1715 -116
- package/dist/HubSpotConnector.js.map +1 -1
- package/dist/YourMembershipConnector.d.ts +160 -5
- package/dist/YourMembershipConnector.d.ts.map +1 -1
- package/dist/YourMembershipConnector.js +3220 -102
- package/dist/YourMembershipConnector.js.map +1 -1
- package/package.json +5 -5
package/dist/HubSpotConnector.js
CHANGED
|
@@ -4,6 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
+
var HubSpotConnector_1;
|
|
7
8
|
import { RegisterClass } from '@memberjunction/global';
|
|
8
9
|
import { Metadata, RunView } from '@memberjunction/core';
|
|
9
10
|
import { BaseIntegrationConnector, BaseRESTIntegrationConnector, } from '@memberjunction/integration-engine';
|
|
@@ -297,6 +298,592 @@ const HUBSPOT_OBJECTS = [
|
|
|
297
298
|
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
298
299
|
],
|
|
299
300
|
},
|
|
301
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
302
|
+
// CRM: Goal Targets (discovered live via Properties API)
|
|
303
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
304
|
+
{
|
|
305
|
+
Name: 'goal_targets', DisplayName: 'Goal Target',
|
|
306
|
+
Description: 'Target milestone or metric threshold for a goal', SupportsWrite: true, IncludeInActionGeneration: false,
|
|
307
|
+
Fields: [
|
|
308
|
+
{ Name: 'hs_goal_name', DisplayName: 'Goal Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Associated goal name' },
|
|
309
|
+
{ Name: 'hs_target_amount', DisplayName: 'Target Amount', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Target value to achieve' },
|
|
310
|
+
{ Name: 'hs_goal_target_kpi_type', DisplayName: 'KPI Type', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'KPI metric type' },
|
|
311
|
+
{ Name: 'hs_start_date', DisplayName: 'Start Date', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Goal target start date' },
|
|
312
|
+
{ Name: 'hs_end_date', DisplayName: 'End Date', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Goal target end date' },
|
|
313
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
314
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
315
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
316
|
+
],
|
|
317
|
+
},
|
|
318
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
319
|
+
// Non-CRM Objects — field definitions for objects without Properties API
|
|
320
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
321
|
+
// ── Pipelines & Stages ───────────────────────────────────────────────
|
|
322
|
+
{
|
|
323
|
+
Name: 'deal_pipelines', DisplayName: 'Deal Pipeline',
|
|
324
|
+
Description: 'Deal pipeline definitions', SupportsWrite: true, IncludeInActionGeneration: false,
|
|
325
|
+
Fields: [
|
|
326
|
+
{ Name: 'id', DisplayName: 'Pipeline ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Pipeline unique identifier' },
|
|
327
|
+
{ Name: 'label', DisplayName: 'Label', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Pipeline display label' },
|
|
328
|
+
{ Name: 'displayOrder', DisplayName: 'Display Order', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Sort order' },
|
|
329
|
+
{ Name: 'archived', DisplayName: 'Archived', Type: 'boolean', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Whether pipeline is archived' },
|
|
330
|
+
{ Name: 'createdAt', DisplayName: 'Created At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
331
|
+
{ Name: 'updatedAt', DisplayName: 'Updated At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
332
|
+
],
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
Name: 'ticket_pipelines', DisplayName: 'Ticket Pipeline',
|
|
336
|
+
Description: 'Ticket pipeline definitions', SupportsWrite: true, IncludeInActionGeneration: false,
|
|
337
|
+
Fields: [
|
|
338
|
+
{ Name: 'id', DisplayName: 'Pipeline ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Pipeline unique identifier' },
|
|
339
|
+
{ Name: 'label', DisplayName: 'Label', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Pipeline display label' },
|
|
340
|
+
{ Name: 'displayOrder', DisplayName: 'Display Order', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Sort order' },
|
|
341
|
+
{ Name: 'archived', DisplayName: 'Archived', Type: 'boolean', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Whether pipeline is archived' },
|
|
342
|
+
{ Name: 'createdAt', DisplayName: 'Created At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
343
|
+
{ Name: 'updatedAt', DisplayName: 'Updated At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
344
|
+
],
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
Name: 'deal_pipeline_stages', DisplayName: 'Deal Pipeline Stage',
|
|
348
|
+
Description: 'Stages within deal pipelines', SupportsWrite: true, IncludeInActionGeneration: false,
|
|
349
|
+
Fields: [
|
|
350
|
+
{ Name: 'id', DisplayName: 'Stage ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Stage unique identifier' },
|
|
351
|
+
{ Name: 'label', DisplayName: 'Label', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Stage display name' },
|
|
352
|
+
{ Name: 'displayOrder', DisplayName: 'Display Order', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Sort order within pipeline' },
|
|
353
|
+
{ Name: 'metadata', DisplayName: 'Metadata', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Stage metadata JSON (probability, isClosed)' },
|
|
354
|
+
{ Name: 'writePermissions', DisplayName: 'Write Permissions', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Write permission setting' },
|
|
355
|
+
{ Name: 'archived', DisplayName: 'Archived', Type: 'boolean', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Whether stage is archived' },
|
|
356
|
+
{ Name: 'createdAt', DisplayName: 'Created At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
357
|
+
{ Name: 'updatedAt', DisplayName: 'Updated At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
358
|
+
],
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
Name: 'ticket_pipeline_stages', DisplayName: 'Ticket Pipeline Stage',
|
|
362
|
+
Description: 'Stages within ticket pipelines', SupportsWrite: true, IncludeInActionGeneration: false,
|
|
363
|
+
Fields: [
|
|
364
|
+
{ Name: 'id', DisplayName: 'Stage ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Stage unique identifier' },
|
|
365
|
+
{ Name: 'label', DisplayName: 'Label', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Stage display name' },
|
|
366
|
+
{ Name: 'displayOrder', DisplayName: 'Display Order', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Sort order within pipeline' },
|
|
367
|
+
{ Name: 'metadata', DisplayName: 'Metadata', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Stage metadata JSON (ticketState)' },
|
|
368
|
+
{ Name: 'writePermissions', DisplayName: 'Write Permissions', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Write permission setting' },
|
|
369
|
+
{ Name: 'archived', DisplayName: 'Archived', Type: 'boolean', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Whether stage is archived' },
|
|
370
|
+
{ Name: 'createdAt', DisplayName: 'Created At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
371
|
+
{ Name: 'updatedAt', DisplayName: 'Updated At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
372
|
+
],
|
|
373
|
+
},
|
|
374
|
+
// ── Lists Sub-Resources ──────────────────────────────────────────────
|
|
375
|
+
{
|
|
376
|
+
Name: 'list_memberships', DisplayName: 'List Membership',
|
|
377
|
+
Description: 'Records belonging to a contact or company list', SupportsWrite: true, IncludeInActionGeneration: false,
|
|
378
|
+
Fields: [
|
|
379
|
+
{ Name: 'recordId', DisplayName: 'Record ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'CRM record ID that is a member' },
|
|
380
|
+
{ Name: 'listId', DisplayName: 'List ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'ID of the parent list' },
|
|
381
|
+
{ Name: 'membershipTimestamp', DisplayName: 'Membership Timestamp', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When the record joined the list' },
|
|
382
|
+
{ Name: 'listVersion', DisplayName: 'List Version', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'List version when record was added' },
|
|
383
|
+
],
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
Name: 'list_folders', DisplayName: 'List Folder',
|
|
387
|
+
Description: 'Organizational folders for grouping lists', SupportsWrite: true, IncludeInActionGeneration: false,
|
|
388
|
+
Fields: [
|
|
389
|
+
{ Name: 'folderId', DisplayName: 'Folder ID', Type: 'number', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Folder unique identifier' },
|
|
390
|
+
{ Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Folder name' },
|
|
391
|
+
{ Name: 'parentFolderId', DisplayName: 'Parent Folder ID', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Parent folder ID (0 for root)' },
|
|
392
|
+
],
|
|
393
|
+
},
|
|
394
|
+
// ── CRM Imports & Exports ────────────────────────────────────────────
|
|
395
|
+
{
|
|
396
|
+
Name: 'crm_imports', DisplayName: 'CRM Import',
|
|
397
|
+
Description: 'Import job history and status', SupportsWrite: false, IncludeInActionGeneration: false,
|
|
398
|
+
Fields: [
|
|
399
|
+
{ Name: 'id', DisplayName: 'Import ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Import job identifier' },
|
|
400
|
+
{ Name: 'state', DisplayName: 'State', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Import state (STARTED, PROCESSING, DONE, FAILED, CANCELED)' },
|
|
401
|
+
{ Name: 'optOutImport', DisplayName: 'Opt Out Import', Type: 'boolean', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Whether import was opt-out type' },
|
|
402
|
+
{ Name: 'createdAt', DisplayName: 'Created At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When import was initiated' },
|
|
403
|
+
{ Name: 'updatedAt', DisplayName: 'Updated At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When import status last changed' },
|
|
404
|
+
],
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
Name: 'crm_exports', DisplayName: 'CRM Export',
|
|
408
|
+
Description: 'Export job history and status', SupportsWrite: false, IncludeInActionGeneration: false,
|
|
409
|
+
Fields: [
|
|
410
|
+
{ Name: 'id', DisplayName: 'Export ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Export task identifier' },
|
|
411
|
+
{ Name: 'status', DisplayName: 'Status', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Export status (PENDING, PROCESSING, COMPLETE)' },
|
|
412
|
+
{ Name: 'createdAt', DisplayName: 'Created At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When export started' },
|
|
413
|
+
],
|
|
414
|
+
},
|
|
415
|
+
// ── Transactional Email ──────────────────────────────────────────────
|
|
416
|
+
{
|
|
417
|
+
Name: 'transactional_smtp_tokens', DisplayName: 'Transactional SMTP Token',
|
|
418
|
+
Description: 'SMTP API tokens for transactional email sending', SupportsWrite: true, IncludeInActionGeneration: false,
|
|
419
|
+
Fields: [
|
|
420
|
+
{ Name: 'id', DisplayName: 'Token ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Token unique identifier' },
|
|
421
|
+
{ Name: 'emailAddress', DisplayName: 'Email Address', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Sender email address' },
|
|
422
|
+
{ Name: 'createdBy', DisplayName: 'Created By', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'User who created the token' },
|
|
423
|
+
{ Name: 'emailCampaignId', DisplayName: 'Email Campaign ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Associated email campaign' },
|
|
424
|
+
{ Name: 'campaignName', DisplayName: 'Campaign Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Campaign label for grouping' },
|
|
425
|
+
{ Name: 'createdAt', DisplayName: 'Created At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
426
|
+
],
|
|
427
|
+
},
|
|
428
|
+
// ── HubDB Rows ───────────────────────────────────────────────────────
|
|
429
|
+
{
|
|
430
|
+
Name: 'hubdb_rows', DisplayName: 'HubDB Row',
|
|
431
|
+
Description: 'Row data within a HubDB structured table', SupportsWrite: true, IncludeInActionGeneration: false,
|
|
432
|
+
Fields: [
|
|
433
|
+
{ Name: 'id', DisplayName: 'Row ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Row unique identifier' },
|
|
434
|
+
{ Name: 'path', DisplayName: 'Path', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'URL path for the row' },
|
|
435
|
+
{ Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Row display name' },
|
|
436
|
+
{ Name: 'childTableId', DisplayName: 'Child Table ID', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Nested child table reference' },
|
|
437
|
+
{ Name: 'createdAt', DisplayName: 'Created At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
438
|
+
{ Name: 'updatedAt', DisplayName: 'Updated At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
439
|
+
],
|
|
440
|
+
},
|
|
441
|
+
// ── Automation — Custom Coded Actions ─────────────────────────────────
|
|
442
|
+
{
|
|
443
|
+
Name: 'custom_coded_actions', DisplayName: 'Custom Coded Action',
|
|
444
|
+
Description: 'Developer-created workflow extension action definitions', SupportsWrite: true, IncludeInActionGeneration: false,
|
|
445
|
+
Fields: [
|
|
446
|
+
{ Name: 'id', DisplayName: 'Action ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Action definition identifier' },
|
|
447
|
+
{ Name: 'actionUrl', DisplayName: 'Action URL', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Webhook URL for action execution' },
|
|
448
|
+
{ Name: 'published', DisplayName: 'Published', Type: 'boolean', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Whether action is published' },
|
|
449
|
+
{ Name: 'revisionId', DisplayName: 'Revision ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Current revision identifier' },
|
|
450
|
+
{ Name: 'archivedAt', DisplayName: 'Archived At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When archived (null if active)' },
|
|
451
|
+
],
|
|
452
|
+
},
|
|
453
|
+
// ── Files — Folders ──────────────────────────────────────────────────
|
|
454
|
+
{
|
|
455
|
+
Name: 'file_folders', DisplayName: 'File Folder',
|
|
456
|
+
Description: 'File manager folder structure', SupportsWrite: true, IncludeInActionGeneration: false,
|
|
457
|
+
Fields: [
|
|
458
|
+
{ Name: 'id', DisplayName: 'Folder ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Folder unique identifier' },
|
|
459
|
+
{ Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Folder name' },
|
|
460
|
+
{ Name: 'path', DisplayName: 'Path', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Full path to the folder' },
|
|
461
|
+
{ Name: 'parentFolderId', DisplayName: 'Parent Folder ID', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Parent folder for nesting' },
|
|
462
|
+
{ Name: 'archived', DisplayName: 'Archived', Type: 'boolean', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Whether folder is archived' },
|
|
463
|
+
{ Name: 'createdAt', DisplayName: 'Created At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
464
|
+
{ Name: 'updatedAt', DisplayName: 'Updated At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
465
|
+
],
|
|
466
|
+
},
|
|
467
|
+
// ── Account & Settings ───────────────────────────────────────────────
|
|
468
|
+
{
|
|
469
|
+
Name: 'account_info', DisplayName: 'Account Info',
|
|
470
|
+
Description: 'HubSpot portal account details and configuration', SupportsWrite: false, IncludeInActionGeneration: false,
|
|
471
|
+
Fields: [
|
|
472
|
+
{ Name: 'portalId', DisplayName: 'Portal ID', Type: 'number', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot portal (account) ID' },
|
|
473
|
+
{ Name: 'accountType', DisplayName: 'Account Type', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Account/subscription type' },
|
|
474
|
+
{ Name: 'timeZone', DisplayName: 'Time Zone', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Portal default time zone' },
|
|
475
|
+
{ Name: 'companyCurrency', DisplayName: 'Company Currency', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Default currency code' },
|
|
476
|
+
{ Name: 'additionalCurrencies', DisplayName: 'Additional Currencies', Type: 'text', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Enabled additional currencies' },
|
|
477
|
+
{ Name: 'utcOffset', DisplayName: 'UTC Offset', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'UTC offset string' },
|
|
478
|
+
{ Name: 'utcOffsetMilliseconds', DisplayName: 'UTC Offset (ms)', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'UTC offset in milliseconds' },
|
|
479
|
+
{ Name: 'uiDomain', DisplayName: 'UI Domain', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Portal UI domain' },
|
|
480
|
+
{ Name: 'dataHostingLocation', DisplayName: 'Data Hosting Location', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Data residency region' },
|
|
481
|
+
],
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
Name: 'api_usage', DisplayName: 'API Usage',
|
|
485
|
+
Description: 'Daily API call usage statistics', SupportsWrite: false, IncludeInActionGeneration: false,
|
|
486
|
+
Fields: [
|
|
487
|
+
{ Name: 'name', DisplayName: 'App Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Private app name' },
|
|
488
|
+
{ Name: 'usageCount', DisplayName: 'Usage Count', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Number of API calls' },
|
|
489
|
+
{ Name: 'currentUsage', DisplayName: 'Current Usage', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Current usage for the period' },
|
|
490
|
+
{ Name: 'collectedAt', DisplayName: 'Collected At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When data was collected' },
|
|
491
|
+
{ Name: 'fetchStatus', DisplayName: 'Fetch Status', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Status of the data fetch' },
|
|
492
|
+
{ Name: 'resetsAt', DisplayName: 'Resets At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When usage counter resets' },
|
|
493
|
+
],
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
Name: 'portal_users', DisplayName: 'Portal User',
|
|
497
|
+
Description: 'HubSpot portal users with role and team assignments', SupportsWrite: true, IncludeInActionGeneration: false,
|
|
498
|
+
Fields: [
|
|
499
|
+
{ Name: 'id', DisplayName: 'User ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'User unique identifier' },
|
|
500
|
+
{ Name: 'email', DisplayName: 'Email', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'User email address' },
|
|
501
|
+
{ Name: 'roleId', DisplayName: 'Role ID', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Assigned role identifier' },
|
|
502
|
+
{ Name: 'primaryTeamId', DisplayName: 'Primary Team ID', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Primary team assignment' },
|
|
503
|
+
{ Name: 'superAdmin', DisplayName: 'Super Admin', Type: 'boolean', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Whether user is a super admin' },
|
|
504
|
+
],
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
Name: 'user_roles', DisplayName: 'User Role',
|
|
508
|
+
Description: 'Portal user role definitions', SupportsWrite: false, IncludeInActionGeneration: false,
|
|
509
|
+
Fields: [
|
|
510
|
+
{ Name: 'id', DisplayName: 'Role ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Role unique identifier' },
|
|
511
|
+
{ Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'Role name' },
|
|
512
|
+
{ Name: 'requiresBillingWrite', DisplayName: 'Requires Billing Write', Type: 'boolean', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Whether role requires billing write' },
|
|
513
|
+
],
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
Name: 'business_units', DisplayName: 'Business Unit',
|
|
517
|
+
Description: 'Business unit partitions within a portal', SupportsWrite: false, IncludeInActionGeneration: false,
|
|
518
|
+
Fields: [
|
|
519
|
+
{ Name: 'id', DisplayName: 'Business Unit ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Business unit identifier' },
|
|
520
|
+
{ Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'Business unit name' },
|
|
521
|
+
{ Name: 'logoMetadata', DisplayName: 'Logo Metadata', Type: 'text', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Logo configuration JSON' },
|
|
522
|
+
],
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
Name: 'currencies', DisplayName: 'Currency',
|
|
526
|
+
Description: 'Exchange rate and currency settings', SupportsWrite: true, IncludeInActionGeneration: false,
|
|
527
|
+
Fields: [
|
|
528
|
+
{ Name: 'id', DisplayName: 'Exchange Rate ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Exchange rate record identifier' },
|
|
529
|
+
{ Name: 'fromCurrencyCode', DisplayName: 'From Currency', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Source currency code (ISO 4217)' },
|
|
530
|
+
{ Name: 'toCurrencyCode', DisplayName: 'To Currency', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Target currency code (ISO 4217)' },
|
|
531
|
+
{ Name: 'conversionRate', DisplayName: 'Conversion Rate', Type: 'number', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Exchange rate value' },
|
|
532
|
+
{ Name: 'effectiveTimestamp', DisplayName: 'Effective At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When rate became effective' },
|
|
533
|
+
{ Name: 'visible', DisplayName: 'Visible', Type: 'boolean', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Whether currency pair is visible' },
|
|
534
|
+
{ Name: 'createdAt', DisplayName: 'Created At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
535
|
+
{ Name: 'updatedAt', DisplayName: 'Updated At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
536
|
+
],
|
|
537
|
+
},
|
|
538
|
+
// ── Conversations ────────────────────────────────────────────────────
|
|
539
|
+
{
|
|
540
|
+
Name: 'conversation_inboxes', DisplayName: 'Conversation Inbox',
|
|
541
|
+
Description: 'Conversations inbox definitions', SupportsWrite: false, IncludeInActionGeneration: false,
|
|
542
|
+
Fields: [
|
|
543
|
+
{ Name: 'id', DisplayName: 'Inbox ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Inbox unique identifier' },
|
|
544
|
+
{ Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: false, Description: 'Inbox name' },
|
|
545
|
+
{ Name: 'archived', DisplayName: 'Archived', Type: 'boolean', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Whether inbox is archived' },
|
|
546
|
+
{ Name: 'createdAt', DisplayName: 'Created At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
547
|
+
{ Name: 'updatedAt', DisplayName: 'Updated At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
548
|
+
],
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
Name: 'conversation_inbox_channels', DisplayName: 'Conversation Inbox Channel',
|
|
552
|
+
Description: 'Communication channels attached to a conversations inbox', SupportsWrite: false, IncludeInActionGeneration: false,
|
|
553
|
+
Fields: [
|
|
554
|
+
{ Name: 'channelId', DisplayName: 'Channel ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Channel unique identifier' },
|
|
555
|
+
{ Name: 'channelAccountId', DisplayName: 'Channel Account ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Account ID for the channel' },
|
|
556
|
+
{ Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Channel display name' },
|
|
557
|
+
{ Name: 'type', DisplayName: 'Type', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Channel type (EMAIL, CHAT, FORM)' },
|
|
558
|
+
{ Name: 'createdAt', DisplayName: 'Created At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
559
|
+
{ Name: 'updatedAt', DisplayName: 'Updated At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
560
|
+
],
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
Name: 'conversation_custom_channels', DisplayName: 'Conversation Custom Channel',
|
|
564
|
+
Description: 'Developer-registered custom communication channels', SupportsWrite: true, IncludeInActionGeneration: false,
|
|
565
|
+
Fields: [
|
|
566
|
+
{ Name: 'id', DisplayName: 'Channel ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Custom channel identifier' },
|
|
567
|
+
{ Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Channel display name' },
|
|
568
|
+
{ Name: 'webhookUrl', DisplayName: 'Webhook URL', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Webhook URL for receiving messages' },
|
|
569
|
+
{ Name: 'channelAccountConnectUrl', DisplayName: 'Connect URL', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'URL for connecting channel accounts' },
|
|
570
|
+
{ Name: 'createdAt', DisplayName: 'Created At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
571
|
+
{ Name: 'updatedAt', DisplayName: 'Updated At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
572
|
+
],
|
|
573
|
+
},
|
|
574
|
+
// ── Timeline Events ──────────────────────────────────────────────────
|
|
575
|
+
{
|
|
576
|
+
Name: 'timeline_event_templates', DisplayName: 'Timeline Event Template',
|
|
577
|
+
Description: 'Custom timeline event type definitions', SupportsWrite: true, IncludeInActionGeneration: false,
|
|
578
|
+
Fields: [
|
|
579
|
+
{ Name: 'id', DisplayName: 'Template ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Event template identifier' },
|
|
580
|
+
{ Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Template name' },
|
|
581
|
+
{ Name: 'objectType', DisplayName: 'Object Type', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'CRM object type this event applies to' },
|
|
582
|
+
{ Name: 'headerTemplate', DisplayName: 'Header Template', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Handlebars template for event header' },
|
|
583
|
+
{ Name: 'detailTemplate', DisplayName: 'Detail Template', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Handlebars template for event detail' },
|
|
584
|
+
{ Name: 'createdAt', DisplayName: 'Created At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
585
|
+
{ Name: 'updatedAt', DisplayName: 'Updated At', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
586
|
+
],
|
|
587
|
+
},
|
|
588
|
+
// ── Analytics / Legacy ───────────────────────────────────────────────
|
|
589
|
+
{
|
|
590
|
+
Name: 'email_campaigns_legacy', DisplayName: 'Email Campaign (Legacy)',
|
|
591
|
+
Description: 'Email campaign tracking and analytics', SupportsWrite: false, IncludeInActionGeneration: false,
|
|
592
|
+
Fields: [
|
|
593
|
+
{ Name: 'id', DisplayName: 'Campaign ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Campaign unique identifier' },
|
|
594
|
+
{ Name: 'appId', DisplayName: 'App ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'App that created the campaign' },
|
|
595
|
+
{ Name: 'appName', DisplayName: 'App Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'App display name' },
|
|
596
|
+
{ Name: 'contentId', DisplayName: 'Content ID', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Associated content identifier' },
|
|
597
|
+
{ Name: 'subject', DisplayName: 'Subject', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Email subject line' },
|
|
598
|
+
{ Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Campaign name' },
|
|
599
|
+
{ Name: 'type', DisplayName: 'Type', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Campaign type' },
|
|
600
|
+
{ Name: 'numIncluded', DisplayName: 'Num Included', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Number of recipients included' },
|
|
601
|
+
{ Name: 'numQueued', DisplayName: 'Num Queued', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Number of emails queued' },
|
|
602
|
+
{ Name: 'subType', DisplayName: 'Sub Type', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Campaign sub-type' },
|
|
603
|
+
{ Name: 'lastUpdatedTime', DisplayName: 'Last Updated', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last updated' },
|
|
604
|
+
],
|
|
605
|
+
},
|
|
606
|
+
// ── Missing T1 CRM objects (in STANDARD_OBJECTS but not previously listed here) ─
|
|
607
|
+
{
|
|
608
|
+
Name: 'leads', DisplayName: 'Lead',
|
|
609
|
+
Description: 'A prospective buyer in the lead pipeline (separate from contact lifecycle stage)', SupportsWrite: true,
|
|
610
|
+
Fields: [
|
|
611
|
+
{ Name: 'hs_lead_label', DisplayName: 'Lead Label', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Display label for the lead' },
|
|
612
|
+
{ Name: 'hs_is_enrolled_in_sequence', DisplayName: 'Enrolled in Sequence', Type: 'boolean', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Whether enrolled in a sequence' },
|
|
613
|
+
{ Name: 'hs_lead_status', DisplayName: 'Lead Status', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Lead qualification status' },
|
|
614
|
+
{ Name: 'hubspot_owner_id', DisplayName: 'Owner', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Assigned owner user ID' },
|
|
615
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
616
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
617
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
618
|
+
],
|
|
619
|
+
},
|
|
620
|
+
// ── Engagement / Activity objects ────────────────────────────────────
|
|
621
|
+
{
|
|
622
|
+
Name: 'communications', DisplayName: 'Communication',
|
|
623
|
+
Description: 'SMS, WhatsApp, and LinkedIn messages logged as CRM engagements', SupportsWrite: true,
|
|
624
|
+
Fields: [
|
|
625
|
+
{ Name: 'hs_communication_channel_type', DisplayName: 'Channel Type', Type: 'enum', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Channel (SMS, WHATS_APP, LINKEDIN_MESSAGE)' },
|
|
626
|
+
{ Name: 'hs_communication_body', DisplayName: 'Body', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Message body' },
|
|
627
|
+
{ Name: 'hs_communication_logged_from', DisplayName: 'Logged From', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Source of the logged communication' },
|
|
628
|
+
{ Name: 'hs_timestamp', DisplayName: 'Timestamp', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'When the communication occurred' },
|
|
629
|
+
{ Name: 'hubspot_owner_id', DisplayName: 'Owner', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Assigned owner user ID' },
|
|
630
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
631
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
632
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
633
|
+
],
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
Name: 'postal_mail', DisplayName: 'Postal Mail',
|
|
637
|
+
Description: 'Physical mail logged as a CRM engagement', SupportsWrite: true,
|
|
638
|
+
Fields: [
|
|
639
|
+
{ Name: 'hs_body_preview', DisplayName: 'Body Preview', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Preview of the mail content' },
|
|
640
|
+
{ Name: 'hs_timestamp', DisplayName: 'Timestamp', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'When the mail was sent' },
|
|
641
|
+
{ Name: 'hs_postal_mail_status', DisplayName: 'Status', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Delivery status' },
|
|
642
|
+
{ Name: 'hubspot_owner_id', DisplayName: 'Owner', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Assigned owner user ID' },
|
|
643
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
644
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
645
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
646
|
+
],
|
|
647
|
+
},
|
|
648
|
+
// ── Commerce objects ─────────────────────────────────────────────────
|
|
649
|
+
{
|
|
650
|
+
Name: 'invoices', DisplayName: 'Invoice',
|
|
651
|
+
Description: 'Billing invoice records in HubSpot Commerce', SupportsWrite: true,
|
|
652
|
+
Fields: [
|
|
653
|
+
{ Name: 'hs_invoice_status', DisplayName: 'Status', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Invoice status (DRAFT, OUTSTANDING, PAID, VOIDED)' },
|
|
654
|
+
{ Name: 'hs_number', DisplayName: 'Invoice Number', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Invoice number' },
|
|
655
|
+
{ Name: 'hs_due_date', DisplayName: 'Due Date', Type: 'date', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Payment due date' },
|
|
656
|
+
{ Name: 'hs_currency_code', DisplayName: 'Currency', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'ISO currency code' },
|
|
657
|
+
{ Name: 'hs_invoice_total_amount', DisplayName: 'Total Amount', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Total invoice amount' },
|
|
658
|
+
{ Name: 'hubspot_owner_id', DisplayName: 'Owner', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Assigned owner user ID' },
|
|
659
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
660
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
661
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
662
|
+
],
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
Name: 'subscriptions', DisplayName: 'Commerce Subscription',
|
|
666
|
+
Description: 'Recurring commerce subscriptions', SupportsWrite: false, IncludeInActionGeneration: false,
|
|
667
|
+
Fields: [
|
|
668
|
+
{ Name: 'hs_status', DisplayName: 'Status', Type: 'enum', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Subscription status (ACTIVE, CANCELLED, PAST_DUE, etc.)' },
|
|
669
|
+
{ Name: 'hs_billing_start_date', DisplayName: 'Billing Start', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When billing started' },
|
|
670
|
+
{ Name: 'hs_recurring_billing_period', DisplayName: 'Billing Period', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Billing frequency period' },
|
|
671
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
672
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
673
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
674
|
+
],
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
Name: 'discounts', DisplayName: 'Discount',
|
|
678
|
+
Description: 'Discount line items applied to commerce transactions', SupportsWrite: true,
|
|
679
|
+
Fields: [
|
|
680
|
+
{ Name: 'hs_label', DisplayName: 'Label', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Discount label' },
|
|
681
|
+
{ Name: 'hs_discount_percentage', DisplayName: 'Discount %', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Percentage discount amount' },
|
|
682
|
+
{ Name: 'hs_value', DisplayName: 'Value', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Fixed discount value' },
|
|
683
|
+
{ Name: 'hs_type', DisplayName: 'Type', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Discount type (PERCENT or FIXED_AMOUNT)' },
|
|
684
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
685
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
686
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
687
|
+
],
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
Name: 'fees', DisplayName: 'Fee',
|
|
691
|
+
Description: 'Fee line items applied to commerce transactions', SupportsWrite: true,
|
|
692
|
+
Fields: [
|
|
693
|
+
{ Name: 'hs_label', DisplayName: 'Label', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Fee label' },
|
|
694
|
+
{ Name: 'hs_value', DisplayName: 'Value', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Fee amount' },
|
|
695
|
+
{ Name: 'hs_type', DisplayName: 'Type', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Fee type (PERCENT or FIXED_AMOUNT)' },
|
|
696
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
697
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
698
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
699
|
+
],
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
Name: 'taxes', DisplayName: 'Tax',
|
|
703
|
+
Description: 'Tax line items applied to commerce transactions', SupportsWrite: true,
|
|
704
|
+
Fields: [
|
|
705
|
+
{ Name: 'hs_label', DisplayName: 'Label', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Tax label' },
|
|
706
|
+
{ Name: 'hs_rate', DisplayName: 'Rate', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Tax rate percentage' },
|
|
707
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
708
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
709
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
710
|
+
],
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
Name: 'commerce_payments', DisplayName: 'Commerce Payment',
|
|
714
|
+
Description: 'Payment records for commerce transactions', SupportsWrite: false, IncludeInActionGeneration: false,
|
|
715
|
+
Fields: [
|
|
716
|
+
{ Name: 'hs_payment_amount', DisplayName: 'Amount', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Payment amount' },
|
|
717
|
+
{ Name: 'hs_payment_status', DisplayName: 'Status', Type: 'enum', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Payment status' },
|
|
718
|
+
{ Name: 'hs_payment_date', DisplayName: 'Payment Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When payment was made' },
|
|
719
|
+
{ Name: 'hs_currency_code', DisplayName: 'Currency', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'ISO currency code' },
|
|
720
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
721
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
722
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
723
|
+
],
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
Name: 'users', DisplayName: 'CRM User',
|
|
727
|
+
Description: 'HubSpot user records exposed via CRM API (distinct from portal settings users)', SupportsWrite: false, IncludeInActionGeneration: false,
|
|
728
|
+
Fields: [
|
|
729
|
+
{ Name: 'hs_email', DisplayName: 'Email', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'User email address' },
|
|
730
|
+
{ Name: 'hs_given_name', DisplayName: 'First Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'First name' },
|
|
731
|
+
{ Name: 'hs_family_name', DisplayName: 'Last Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Last name' },
|
|
732
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
733
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
734
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
735
|
+
],
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
Name: 'orders', DisplayName: 'Order',
|
|
739
|
+
Description: 'Commerce order records', SupportsWrite: true,
|
|
740
|
+
Fields: [
|
|
741
|
+
{ Name: 'hs_order_name', DisplayName: 'Order Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Name of the order' },
|
|
742
|
+
{ Name: 'hs_status', DisplayName: 'Status', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Order status' },
|
|
743
|
+
{ Name: 'hs_fulfillment_status', DisplayName: 'Fulfillment Status', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Fulfillment status' },
|
|
744
|
+
{ Name: 'hs_currency_code', DisplayName: 'Currency', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'ISO currency code' },
|
|
745
|
+
{ Name: 'hs_total_price', DisplayName: 'Total Price', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Total order price' },
|
|
746
|
+
{ Name: 'hs_source_store', DisplayName: 'Source Store', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Source store/channel' },
|
|
747
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
748
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
749
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
750
|
+
],
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
Name: 'carts', DisplayName: 'Cart',
|
|
754
|
+
Description: 'Shopping cart records for commerce', SupportsWrite: true,
|
|
755
|
+
Fields: [
|
|
756
|
+
{ Name: 'hs_cart_name', DisplayName: 'Cart Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Cart name or identifier' },
|
|
757
|
+
{ Name: 'hs_status', DisplayName: 'Status', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Cart status (OPEN, CONVERTED, ABANDONED)' },
|
|
758
|
+
{ Name: 'hs_total_price', DisplayName: 'Total Price', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Total cart value' },
|
|
759
|
+
{ Name: 'hs_currency_code', DisplayName: 'Currency', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'ISO currency code' },
|
|
760
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
761
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
762
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
763
|
+
],
|
|
764
|
+
},
|
|
765
|
+
// ── Marketing ────────────────────────────────────────────────────────
|
|
766
|
+
{
|
|
767
|
+
Name: 'marketing_events', DisplayName: 'Marketing Event',
|
|
768
|
+
Description: 'Virtual or in-person events tracked in HubSpot', SupportsWrite: true,
|
|
769
|
+
Fields: [
|
|
770
|
+
{ Name: 'eventName', DisplayName: 'Event Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Name of the marketing event' },
|
|
771
|
+
{ Name: 'eventType', DisplayName: 'Event Type', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Type of event (WEBINAR, CONFERENCE, etc.)' },
|
|
772
|
+
{ Name: 'eventOrganizer', DisplayName: 'Organizer', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Event organizer name' },
|
|
773
|
+
{ Name: 'eventUrl', DisplayName: 'Event URL', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Link to the event' },
|
|
774
|
+
{ Name: 'startDateTime', DisplayName: 'Start Date/Time', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Event start date and time' },
|
|
775
|
+
{ Name: 'endDateTime', DisplayName: 'End Date/Time', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Event end date and time' },
|
|
776
|
+
{ Name: 'registrants', DisplayName: 'Registrants', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Number of registered attendees' },
|
|
777
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
778
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
779
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
780
|
+
],
|
|
781
|
+
},
|
|
782
|
+
// ── Activatable Objects (Object Library) ─────────────────────────────
|
|
783
|
+
{
|
|
784
|
+
Name: 'services', DisplayName: 'Service',
|
|
785
|
+
Description: 'Service offerings tracked in HubSpot (activatable object)', SupportsWrite: true,
|
|
786
|
+
Fields: [
|
|
787
|
+
{ Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Service name' },
|
|
788
|
+
{ Name: 'description', DisplayName: 'Description', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Service description' },
|
|
789
|
+
{ Name: 'price', DisplayName: 'Price', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Service price' },
|
|
790
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
791
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
792
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
793
|
+
],
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
Name: 'courses', DisplayName: 'Course',
|
|
797
|
+
Description: 'Educational courses tracked in HubSpot (activatable object)', SupportsWrite: true,
|
|
798
|
+
Fields: [
|
|
799
|
+
{ Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Course name' },
|
|
800
|
+
{ Name: 'description', DisplayName: 'Description', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Course description' },
|
|
801
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
802
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
803
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
804
|
+
],
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
Name: 'listings', DisplayName: 'Listing',
|
|
808
|
+
Description: 'Property or product listings (activatable object)', SupportsWrite: true,
|
|
809
|
+
Fields: [
|
|
810
|
+
{ Name: 'name', DisplayName: 'Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Listing name' },
|
|
811
|
+
{ Name: 'description', DisplayName: 'Description', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Listing description' },
|
|
812
|
+
{ Name: 'price', DisplayName: 'Price', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Listing price' },
|
|
813
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
814
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
815
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
816
|
+
],
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
Name: 'appointments', DisplayName: 'Appointment',
|
|
820
|
+
Description: 'Scheduled appointments (activatable object)', SupportsWrite: true,
|
|
821
|
+
Fields: [
|
|
822
|
+
{ Name: 'hs_appointment_name', DisplayName: 'Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Appointment name' },
|
|
823
|
+
{ Name: 'hs_appointment_start', DisplayName: 'Start', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Appointment start time' },
|
|
824
|
+
{ Name: 'hs_appointment_end', DisplayName: 'End', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Appointment end time' },
|
|
825
|
+
{ Name: 'hs_appointment_status', DisplayName: 'Status', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Appointment status' },
|
|
826
|
+
{ Name: 'hubspot_owner_id', DisplayName: 'Owner', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Assigned owner user ID' },
|
|
827
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
828
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
829
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
830
|
+
],
|
|
831
|
+
},
|
|
832
|
+
// ── System / Other CRM ───────────────────────────────────────────────
|
|
833
|
+
{
|
|
834
|
+
Name: 'projects', DisplayName: 'Project',
|
|
835
|
+
Description: 'Project records in HubSpot CRM', SupportsWrite: true,
|
|
836
|
+
Fields: [
|
|
837
|
+
{ Name: 'hs_project_name', DisplayName: 'Project Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Name of the project' },
|
|
838
|
+
{ Name: 'hs_status', DisplayName: 'Status', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Project status' },
|
|
839
|
+
{ Name: 'hs_start_date', DisplayName: 'Start Date', Type: 'date', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Project start date' },
|
|
840
|
+
{ Name: 'hs_due_date', DisplayName: 'Due Date', Type: 'date', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Project due date' },
|
|
841
|
+
{ Name: 'hubspot_owner_id', DisplayName: 'Owner', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Assigned owner user ID' },
|
|
842
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
843
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
844
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
845
|
+
],
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
Name: 'deal_splits', DisplayName: 'Deal Split',
|
|
849
|
+
Description: 'Revenue split allocations across deal owners', SupportsWrite: true,
|
|
850
|
+
Fields: [
|
|
851
|
+
{ Name: 'hs_split_percentage', DisplayName: 'Split %', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Percentage of deal revenue attributed to this split' },
|
|
852
|
+
{ Name: 'hs_split_amount', DisplayName: 'Split Amount', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Calculated split amount' },
|
|
853
|
+
{ Name: 'hs_deal_id', DisplayName: 'Deal ID', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Associated deal ID' },
|
|
854
|
+
{ Name: 'hubspot_owner_id', DisplayName: 'Owner', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Owner receiving the split credit' },
|
|
855
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
856
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
857
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
858
|
+
],
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
Name: 'transcriptions', DisplayName: 'Transcription',
|
|
862
|
+
Description: 'Call transcription records (auto-generated by HubSpot AI, read-only)', SupportsWrite: false, IncludeInActionGeneration: false,
|
|
863
|
+
Fields: [
|
|
864
|
+
{ Name: 'hs_call_id', DisplayName: 'Call ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'ID of the associated call' },
|
|
865
|
+
{ Name: 'hs_transcript_text', DisplayName: 'Transcript', Type: 'text', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Full call transcript text' },
|
|
866
|
+
{ Name: 'hs_language', DisplayName: 'Language', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Detected transcript language' },
|
|
867
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
868
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
869
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
870
|
+
],
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
Name: 'contracts', DisplayName: 'Contract',
|
|
874
|
+
Description: 'Contract records in HubSpot CRM', SupportsWrite: true,
|
|
875
|
+
Fields: [
|
|
876
|
+
{ Name: 'hs_title', DisplayName: 'Title', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Contract title' },
|
|
877
|
+
{ Name: 'hs_contract_status', DisplayName: 'Status', Type: 'enum', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Contract status (DRAFT, SENT, SIGNED, etc.)' },
|
|
878
|
+
{ Name: 'hs_effective_date', DisplayName: 'Effective Date', Type: 'date', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Date the contract takes effect' },
|
|
879
|
+
{ Name: 'hs_expiration_date', DisplayName: 'Expiration Date', Type: 'date', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Contract expiration date' },
|
|
880
|
+
{ Name: 'hs_total_contract_value', DisplayName: 'Total Value', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Total contract value' },
|
|
881
|
+
{ Name: 'hubspot_owner_id', DisplayName: 'Owner', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Assigned owner user ID' },
|
|
882
|
+
{ Name: 'hs_object_id', DisplayName: 'Object ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'HubSpot internal object ID' },
|
|
883
|
+
{ Name: 'createdate', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When created' },
|
|
884
|
+
{ Name: 'hs_lastmodifieddate', DisplayName: 'Last Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last modified' },
|
|
885
|
+
],
|
|
886
|
+
},
|
|
300
887
|
];
|
|
301
888
|
// ─── Connector ────────────────────────────────────────────────────────
|
|
302
889
|
/**
|
|
@@ -326,29 +913,509 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
|
|
|
326
913
|
this.lastRequestTime = 0;
|
|
327
914
|
/** Resolved config (populated after first Authenticate call) */
|
|
328
915
|
this._config = null;
|
|
916
|
+
/** Cached auth context — reused within a session to avoid redundant credential loads */
|
|
917
|
+
this._cachedAuth = null;
|
|
918
|
+
}
|
|
919
|
+
static { HubSpotConnector_1 = this; }
|
|
920
|
+
// ── Per-instance config accessors (fall back to module-level defaults) ──
|
|
921
|
+
get effectiveMaxRetries() { return this._config?.MaxRetries ?? MAX_RETRIES; }
|
|
922
|
+
get effectiveRequestTimeoutMs() { return this._config?.RequestTimeoutMs ?? REQUEST_TIMEOUT_MS; }
|
|
923
|
+
get effectiveMinRequestIntervalMs() { return this._config?.MinRequestIntervalMs ?? MIN_REQUEST_INTERVAL_MS; }
|
|
924
|
+
// ─── Capability Getters ──────────────────────────────────────────────
|
|
925
|
+
get SupportsCreate() { return true; }
|
|
926
|
+
get SupportsUpdate() { return true; }
|
|
927
|
+
get SupportsDelete() { return true; }
|
|
928
|
+
get SupportsSearch() { return true; }
|
|
929
|
+
get SupportsListing() { return true; }
|
|
930
|
+
get IntegrationName() { return 'HubSpot'; }
|
|
931
|
+
// ─── Action Metadata ─────────────────────────────────────────────────
|
|
932
|
+
GetIntegrationObjects() {
|
|
933
|
+
return HUBSPOT_OBJECTS;
|
|
934
|
+
}
|
|
935
|
+
GetActionGeneratorConfig() {
|
|
936
|
+
const config = super.GetActionGeneratorConfig();
|
|
937
|
+
if (!config)
|
|
938
|
+
return null;
|
|
939
|
+
config.IconClass = 'fa-brands fa-hubspot';
|
|
940
|
+
config.CreateCategory = false; // HubSpot category already exists in metadata/action-categories
|
|
941
|
+
return config;
|
|
942
|
+
}
|
|
943
|
+
// ─── Live API Discovery ─────────────────────────────────────────────
|
|
944
|
+
/** Known standard HubSpot CRM object type IDs → API names. */
|
|
945
|
+
/**
|
|
946
|
+
* All standard CRM object type IDs → names.
|
|
947
|
+
* Fields discovered live via /crm/v3/properties/{objectType}.
|
|
948
|
+
* All support: GET, POST, PATCH, DELETE, search (incremental via hs_lastmodifieddate).
|
|
949
|
+
*/
|
|
950
|
+
static { this.STANDARD_OBJECTS = {
|
|
951
|
+
// Core CRM
|
|
952
|
+
'0-1': 'contacts', '0-2': 'companies', '0-3': 'deals', '0-4': 'leads',
|
|
953
|
+
'0-5': 'tickets', '0-7': 'products', '0-8': 'line_items',
|
|
954
|
+
// Activities
|
|
955
|
+
'0-18': 'communications', '0-19': 'feedback_submissions',
|
|
956
|
+
'0-27': 'tasks', '0-46': 'notes', '0-47': 'meetings',
|
|
957
|
+
'0-48': 'calls', '0-49': 'emails', '0-116': 'postal_mail',
|
|
958
|
+
// Commerce
|
|
959
|
+
'0-14': 'quotes', '0-53': 'invoices', '0-69': 'subscriptions',
|
|
960
|
+
'0-74': 'goal_targets', '0-84': 'discounts', '0-85': 'fees',
|
|
961
|
+
'0-86': 'taxes', '0-101': 'commerce_payments', '0-115': 'users',
|
|
962
|
+
'0-123': 'orders', '0-142': 'carts',
|
|
963
|
+
// Marketing (CRM-backed)
|
|
964
|
+
'0-54': 'marketing_events',
|
|
965
|
+
// Activatable (Object Library)
|
|
966
|
+
'0-162': 'services', '0-410': 'courses', '0-420': 'listings',
|
|
967
|
+
'0-421': 'appointments',
|
|
968
|
+
// System / Other CRM
|
|
969
|
+
// NOTE: deal_splits (0-72), transcriptions (0-150), contracts (0-155) objectTypeIds
|
|
970
|
+
// are confirmed from the HubSpot API catalog as of April 2026 but have not been
|
|
971
|
+
// independently verified against a live account. partner_clients, partner_services,
|
|
972
|
+
// and subscription_lifecycle have TBD objectTypeIds — discovered dynamically via schemas API.
|
|
973
|
+
'0-970': 'projects', '0-72': 'deal_splits', '0-150': 'transcriptions',
|
|
974
|
+
'0-155': 'contracts', '0-136': 'goals',
|
|
975
|
+
}; }
|
|
976
|
+
/**
|
|
977
|
+
* Non-CRM API endpoints that don't follow the /crm/v3/objects pattern.
|
|
978
|
+
* Fields cannot be discovered via /crm/v3/properties — discovered dynamically
|
|
979
|
+
* by fetching one record and inferring fields from the response.
|
|
980
|
+
*/
|
|
981
|
+
static { this.NON_CRM_OBJECTS = [
|
|
982
|
+
// ── CRM Config ───────────────────────────────────────────────────
|
|
983
|
+
{ name: 'owners', label: 'Owners', description: 'HubSpot users who own records', apiPath: '/crm/v3/owners', write: false, incremental: true, pkField: 'id', incrementalParam: 'after' },
|
|
984
|
+
{ name: 'deal_pipelines', label: 'Deal Pipelines', description: 'Deal pipeline definitions and stages', apiPath: '/crm/v3/pipelines/deals', write: true, incremental: false, pkField: 'id' },
|
|
985
|
+
{ name: 'ticket_pipelines', label: 'Ticket Pipelines', description: 'Ticket pipeline definitions and stages', apiPath: '/crm/v3/pipelines/tickets', write: true, incremental: false, pkField: 'id' },
|
|
986
|
+
{ name: 'forecasts', label: 'Forecasts', description: 'CRM sales forecasts', apiPath: '/crm/v3/forecasts', write: false, incremental: false, pkField: 'id' },
|
|
987
|
+
// Pipeline stages — parameterized; falls back to DB fields
|
|
988
|
+
{ name: 'deal_pipeline_stages', label: 'Deal Pipeline Stages', description: 'Stages within deal pipelines', apiPath: '/crm/v3/pipelines/deals/{pipelineId}/stages', write: true, incremental: false, pkField: 'id', parentObject: 'deal_pipelines' },
|
|
989
|
+
{ name: 'ticket_pipeline_stages', label: 'Ticket Pipeline Stages', description: 'Stages within ticket pipelines', apiPath: '/crm/v3/pipelines/tickets/{pipelineId}/stages', write: true, incremental: false, pkField: 'id', parentObject: 'ticket_pipelines' },
|
|
990
|
+
// Lists v3 — The legacy v1 Contact Lists API sunsets April 30, 2026.
|
|
991
|
+
// This connector correctly uses the v3 endpoint which is the migration target.
|
|
992
|
+
{ name: 'lists', label: 'Lists', description: 'Contact and company lists', apiPath: '/crm/v3/lists', write: true, incremental: true, pkField: 'listId' },
|
|
993
|
+
// List memberships — parameterized; fan-out across all lists
|
|
994
|
+
{ name: 'list_memberships', label: 'List Memberships', description: 'Records belonging to a list', apiPath: '/crm/v3/lists/{listId}/memberships', write: true, incremental: false, pkField: 'recordId', parentObject: 'lists' },
|
|
995
|
+
{ name: 'list_folders', label: 'List Folders', description: 'Organizational folders for lists', apiPath: '/crm/v3/lists/folders', write: true, incremental: false, pkField: 'id' },
|
|
996
|
+
// CRM Imports & Exports
|
|
997
|
+
{ name: 'crm_imports', label: 'CRM Imports', description: 'Import job history and status', apiPath: '/crm/v3/imports', write: false, incremental: true, pkField: 'id' },
|
|
998
|
+
{ name: 'crm_exports', label: 'CRM Exports', description: 'Export job history and status', apiPath: '/crm/v3/exports/export/async', write: false, incremental: false, pkField: 'id' },
|
|
999
|
+
// ── Marketing ────────────────────────────────────────────────────
|
|
1000
|
+
{ name: 'marketing_emails', label: 'Marketing Emails', description: 'Marketing email campaigns', apiPath: '/marketing/v3/emails', write: true, incremental: true, pkField: 'id', serverIncrementalParam: 'updatedAfter' },
|
|
1001
|
+
{ name: 'campaigns', label: 'Campaigns', description: 'Marketing campaign tracking', apiPath: '/marketing/v3/campaigns', write: false, incremental: true, pkField: 'id', serverIncrementalParam: 'updatedAfter' },
|
|
1002
|
+
// Forms v3 — DEVELOPER_PREVIEW status in HubSpot API catalog. Use for listing/reading;
|
|
1003
|
+
// creating forms programmatically may not be supported in all plans.
|
|
1004
|
+
{ name: 'forms', label: 'Forms', description: 'HubSpot forms for lead capture', apiPath: '/marketing/v3/forms', write: true, incremental: true, pkField: 'id', serverIncrementalParam: 'updatedAfter' },
|
|
1005
|
+
// Form submissions — legacy v1 endpoint; parameterized (requires formGuid); falls back to DB fields
|
|
1006
|
+
{ name: 'form_submissions', label: 'Form Submissions', description: 'Submitted form data across all forms', apiPath: '/form-integrations/v1/submissions/forms', write: false, incremental: true, pkField: 'submittedAt' },
|
|
1007
|
+
// Transactional email — v3 SMTP tokens for single-send (legacy Transactional Single Send)
|
|
1008
|
+
{ name: 'transactional_smtp_tokens', label: 'Transactional SMTP Tokens', description: 'SMTP API tokens for transactional email', apiPath: '/marketing/v3/transactional/smtp-tokens', write: true, incremental: false, pkField: 'id' },
|
|
1009
|
+
// Single-send v4 — separate from the v3 SMTP token endpoint; DEVELOPER_PREVIEW in catalog
|
|
1010
|
+
{ name: 'single_send_v4', label: 'Single Send (v4)', description: 'Single-send transactional email via Marketing API v4', apiPath: '/marketing/v4/email/single-send', write: true, incremental: false, pkField: 'id' },
|
|
1011
|
+
// Ads
|
|
1012
|
+
{ name: 'ad_campaigns', label: 'Ad Campaigns', description: 'Advertising campaigns across ad networks', apiPath: '/marketing/v3/ads/campaigns', write: false, incremental: true, pkField: 'id', serverIncrementalParam: 'updatedAfter' },
|
|
1013
|
+
{ name: 'ad_accounts', label: 'Ad Accounts', description: 'Connected advertising accounts', apiPath: '/marketing/v3/ads/accounts', write: false, incremental: false, pkField: 'id' },
|
|
1014
|
+
// ── CMS ──────────────────────────────────────────────────────────
|
|
1015
|
+
{ name: 'site_pages', label: 'Site Pages', description: 'CMS website pages', apiPath: '/cms/v3/pages/site-pages', write: true, incremental: true, pkField: 'id', serverIncrementalParam: 'updatedAfter' },
|
|
1016
|
+
{ name: 'landing_pages', label: 'Landing Pages', description: 'CMS landing pages', apiPath: '/cms/v3/pages/landing-pages', write: true, incremental: true, pkField: 'id', serverIncrementalParam: 'updatedAfter' },
|
|
1017
|
+
{ name: 'blog_posts', label: 'Blog Posts', description: 'CMS blog posts', apiPath: '/cms/v3/blogs/posts', write: true, incremental: true, pkField: 'id', serverIncrementalParam: 'updatedAfter' },
|
|
1018
|
+
{ name: 'blog_authors', label: 'Blog Authors', description: 'CMS blog author profiles', apiPath: '/cms/v3/blogs/authors', write: true, incremental: true, pkField: 'id', serverIncrementalParam: 'updatedAfter' },
|
|
1019
|
+
{ name: 'blog_tags', label: 'Blog Tags', description: 'CMS blog tag taxonomy', apiPath: '/cms/v3/blogs/tags', write: true, incremental: true, pkField: 'id', serverIncrementalParam: 'updatedAfter' },
|
|
1020
|
+
{ name: 'blog_settings', label: 'Blog Settings', description: 'CMS blog configuration', apiPath: '/cms/v3/blogs/settings', write: false, incremental: false, pkField: 'id' },
|
|
1021
|
+
{ name: 'domains', label: 'Domains', description: 'Connected domains', apiPath: '/cms/v3/domains', write: false, incremental: false, pkField: 'id' },
|
|
1022
|
+
{ name: 'url_mappings', label: 'URL Mappings', description: 'CMS URL mapping rules', apiPath: '/cms/v3/url-redirects/mapping', write: true, incremental: false, pkField: 'id' },
|
|
1023
|
+
{ name: 'url_redirects', label: 'URL Redirects', description: 'URL redirect rules', apiPath: '/cms/v3/url-redirects', write: true, incremental: false, pkField: 'id' },
|
|
1024
|
+
{ name: 'site_search', label: 'Site Search', description: 'CMS site search index results', apiPath: '/cms/v3/site-search/search', write: false, incremental: false, pkField: 'id' },
|
|
1025
|
+
{ name: 'source_code', label: 'Source Code', description: 'CMS theme and template source files', apiPath: '/cms/v3/source-code/environment/published', write: true, incremental: false, pkField: 'path' },
|
|
1026
|
+
{ name: 'media_bridge', label: 'Media Bridge', description: 'External media provider bridge objects', apiPath: '/cms/v3/media-bridge/objects', write: true, incremental: false, pkField: 'id' },
|
|
1027
|
+
{ name: 'hubdb_tables', label: 'HubDB Tables', description: 'HubDB structured data tables', apiPath: '/cms/v3/hubdb/tables', write: true, incremental: true, pkField: 'id', serverIncrementalParam: 'updatedAfter' },
|
|
1028
|
+
// HubDB rows — parameterized; fan-out across all HubDB tables
|
|
1029
|
+
{ name: 'hubdb_rows', label: 'HubDB Rows', description: 'Row data within HubDB tables', apiPath: '/cms/v3/hubdb/tables/{tableIdOrName}/rows', write: true, incremental: false, pkField: 'id', parentObject: 'hubdb_tables' },
|
|
1030
|
+
// ── Automation ───────────────────────────────────────────────────
|
|
1031
|
+
// v3/workflows is the standard listing endpoint
|
|
1032
|
+
{ name: 'workflows', label: 'Workflows', description: 'Automation workflows', apiPath: '/automation/v3/workflows', write: false, incremental: false, pkField: 'id' },
|
|
1033
|
+
// v4/actions — custom coded actions (requires appId); falls back to DB fields
|
|
1034
|
+
{ name: 'custom_coded_actions', label: 'Custom Coded Actions', description: 'Developer-created workflow extension actions', apiPath: '/automation/v4/actions/{appId}', write: true, incremental: false, pkField: 'id' },
|
|
1035
|
+
// ── Events ───────────────────────────────────────────────────────
|
|
1036
|
+
{ name: 'behavioral_events', label: 'Behavioral Events', description: 'Custom behavioral event completions', apiPath: '/events/v3/events', write: true, incremental: true, pkField: 'id', serverIncrementalParam: 'occurredAfter' },
|
|
1037
|
+
{ name: 'event_definitions', label: 'Event Definitions', description: 'Custom event type definitions', apiPath: '/events/v3/event-definitions', write: true, incremental: false, pkField: 'name' },
|
|
1038
|
+
// Event completions — parameterized; fan-out across all event definitions
|
|
1039
|
+
{ name: 'event_completions', label: 'Event Completions', description: 'Completion records for a specific custom behavioral event type', apiPath: '/events/v3/event-definitions/{eventDefinitionName}/completions', write: false, incremental: true, pkField: 'id', parentObject: 'event_definitions' },
|
|
1040
|
+
// ── Files ────────────────────────────────────────────────────────
|
|
1041
|
+
{ name: 'files', label: 'Files', description: 'File manager files and documents', apiPath: '/files/v3/files', write: true, incremental: true, pkField: 'id', serverIncrementalParam: 'updatedAfter' },
|
|
1042
|
+
{ name: 'file_folders', label: 'File Folders', description: 'File manager folder structure', apiPath: '/files/v3/folders', write: true, incremental: false, pkField: 'id' },
|
|
1043
|
+
// ── Account & Settings ───────────────────────────────────────────
|
|
1044
|
+
{ name: 'account_info', label: 'Account Info', description: 'HubSpot portal account details', apiPath: '/account-info/v3/details', write: false, incremental: false, pkField: 'portalId' },
|
|
1045
|
+
{ name: 'api_usage', label: 'API Usage', description: 'Daily API usage statistics', apiPath: '/account-info/v3/api-usage/daily', write: false, incremental: true, pkField: 'date' },
|
|
1046
|
+
{ name: 'audit_logs', label: 'Audit Logs', description: 'Account activity audit trail', apiPath: '/account-info/v3/audit-logs/activity', write: false, incremental: true, pkField: 'id' },
|
|
1047
|
+
{ name: 'portal_users', label: 'Portal Users', description: 'HubSpot portal users and permissions', apiPath: '/settings/v3/users', write: true, incremental: false, pkField: 'id' },
|
|
1048
|
+
{ name: 'user_roles', label: 'User Roles', description: 'Portal user role definitions', apiPath: '/settings/v3/users/roles', write: false, incremental: false, pkField: 'id' },
|
|
1049
|
+
{ name: 'business_units', label: 'Business Units', description: 'Business unit partitions within a portal', apiPath: '/business-units/v3/business-units', write: false, incremental: false, pkField: 'id' },
|
|
1050
|
+
{ name: 'currencies', label: 'Currencies', description: 'Exchange rate and currency settings', apiPath: '/settings/v3/currencies', write: true, incremental: false, pkField: 'currencyCode' },
|
|
1051
|
+
{ name: 'tax_rates', label: 'Tax Rates', description: 'Tax rate definitions', apiPath: '/tax-rates/v3/tax-rates', write: true, incremental: false, pkField: 'id' },
|
|
1052
|
+
// ── Communication Preferences ────────────────────────────────────
|
|
1053
|
+
{ name: 'subscription_definitions', label: 'Subscription Definitions', description: 'Email subscription types', apiPath: '/communication-preferences/v4/definitions', write: true, incremental: false, pkField: 'id' },
|
|
1054
|
+
// ── User Provisioning (SCIM) ─────────────────────────────────────
|
|
1055
|
+
// Requires Enterprise + SCIM scope. Standard SCIM 2.0 schema.
|
|
1056
|
+
{ name: 'scim_users', label: 'SCIM Users', description: 'User provisioning via SCIM 2.0', apiPath: '/scim/v2/Users', write: true, incremental: true, pkField: 'id', incrementalParam: 'startIndex' },
|
|
1057
|
+
{ name: 'scim_groups', label: 'SCIM Groups', description: 'Group provisioning via SCIM 2.0', apiPath: '/scim/v2/Groups', write: true, incremental: false, pkField: 'id' },
|
|
1058
|
+
// ── Conversations ────────────────────────────────────────────────
|
|
1059
|
+
{ name: 'conversation_inboxes', label: 'Conversation Inboxes', description: 'Conversations inbox definitions', apiPath: '/conversations/v3/conversations/inboxes', write: false, incremental: false, pkField: 'id' },
|
|
1060
|
+
{ name: 'conversation_threads', label: 'Conversation Threads', description: 'Conversations inbox threads', apiPath: '/conversations/v3/conversations/threads', write: false, incremental: true, pkField: 'id', serverIncrementalParam: 'updatedAfter' },
|
|
1061
|
+
// Conversation messages — parameterized; fan-out across threads. NOTE: can be very large
|
|
1062
|
+
// at scale (one fetch per thread). Consider disabling for high-volume portals.
|
|
1063
|
+
{ name: 'conversation_messages', label: 'Conversation Messages', description: 'Messages within conversation threads', apiPath: '/conversations/v3/conversations/threads/{threadId}/messages', write: false, incremental: false, pkField: 'id', parentObject: 'conversation_threads' },
|
|
1064
|
+
// Inbox channels — parameterized; fan-out across inboxes
|
|
1065
|
+
{ name: 'conversation_inbox_channels', label: 'Conversation Inbox Channels', description: 'Communication channels attached to an inbox', apiPath: '/conversations/v3/conversations/inboxes/{inboxId}/channels', write: false, incremental: false, pkField: 'channelId', parentObject: 'conversation_inboxes' },
|
|
1066
|
+
// Custom channels (developer-registered channels)
|
|
1067
|
+
{ name: 'conversation_custom_channels', label: 'Conversation Custom Channels', description: 'Developer-registered custom communication channels', apiPath: '/conversations/custom-channels/v3', write: true, incremental: false, pkField: 'id' },
|
|
1068
|
+
// Conversation channels — generic channel listing
|
|
1069
|
+
{ name: 'conversation_channels', label: 'Conversation Channels', description: 'Communication channels for conversations', apiPath: '/conversations/v3/conversations/channels', write: false, incremental: false, pkField: 'id' },
|
|
1070
|
+
// Visitor identification
|
|
1071
|
+
{ name: 'visitor_identification', label: 'Visitor Identification', description: 'Visitor identification tokens for conversations', apiPath: '/conversations/v3/visitor-identification/tokens/create', write: false, incremental: false, pkField: 'token' },
|
|
1072
|
+
// ── Timeline Events ──────────────────────────────────────────────
|
|
1073
|
+
// Timeline event templates — parameterized (requires appId); falls back to DB fields
|
|
1074
|
+
{ name: 'timeline_event_templates', label: 'Timeline Event Templates', description: 'Custom timeline event type definitions', apiPath: '/integrators/timeline/v3/{appId}/event-templates', write: true, incremental: false, pkField: 'id' },
|
|
1075
|
+
// ── Automation ───────────────────────────────────────────────────
|
|
1076
|
+
{ name: 'sequences', label: 'Sequences', description: 'Sales sequences and enrollment rules', apiPath: '/automation/v3/sequences', write: false, incremental: false, pkField: 'id' },
|
|
1077
|
+
// ── Scheduler ───────────────────────────────────────────────────
|
|
1078
|
+
{ name: 'meeting_scheduler', label: 'Meeting Scheduler', description: 'Meeting booking page configurations', apiPath: '/scheduler/v3/meetings/meeting-links', write: false, incremental: false, pkField: 'id' },
|
|
1079
|
+
// ── Analytics / Reporting (Legacy) ───────────────────────────────
|
|
1080
|
+
{ name: 'email_campaigns_legacy', label: 'Email Campaigns (Legacy)', description: 'Email campaign tracking and analytics', apiPath: '/email/public/v1/campaigns', write: false, incremental: true, pkField: 'id' },
|
|
1081
|
+
// ── Data Studio ─────────────────────────────────────────────────
|
|
1082
|
+
{ name: 'datasource_ingestion', label: 'Datasource Ingestion', description: 'External data source ingestion (beta)', apiPath: '/data-studio/v3/datasource-ingestion', write: true, incremental: false, pkField: 'id' },
|
|
1083
|
+
]; }
|
|
1084
|
+
/**
|
|
1085
|
+
* Association objects use the v4 per-object associations endpoint.
|
|
1086
|
+
* Fields are fixed (two FK columns + association_type) — no live discovery API exists.
|
|
1087
|
+
*/
|
|
1088
|
+
static { this.ASSOCIATION_OBJECTS = [
|
|
1089
|
+
{ name: 'assoc_contacts_companies', label: 'Contact ↔ Company', description: 'Associations between contacts and companies', apiPath: '/crm/v4/associations/contacts/companies', pkFields: ['contact_id', 'company_id'] },
|
|
1090
|
+
{ name: 'assoc_contacts_deals', label: 'Contact ↔ Deal', description: 'Associations between contacts and deals', apiPath: '/crm/v4/associations/contacts/deals', pkFields: ['contact_id', 'deal_id'] },
|
|
1091
|
+
{ name: 'assoc_contacts_tickets', label: 'Contact ↔ Ticket', description: 'Associations between contacts and tickets', apiPath: '/crm/v4/associations/contacts/tickets', pkFields: ['contact_id', 'ticket_id'] },
|
|
1092
|
+
{ name: 'assoc_contacts_calls', label: 'Contact ↔ Call', description: 'Associations between contacts and calls', apiPath: '/crm/v4/associations/contacts/calls', pkFields: ['contact_id', 'call_id'] },
|
|
1093
|
+
{ name: 'assoc_contacts_emails', label: 'Contact ↔ Email', description: 'Associations between contacts and emails', apiPath: '/crm/v4/associations/contacts/emails', pkFields: ['contact_id', 'email_id'] },
|
|
1094
|
+
{ name: 'assoc_contacts_meetings', label: 'Contact ↔ Meeting', description: 'Associations between contacts and meetings', apiPath: '/crm/v4/associations/contacts/meetings', pkFields: ['contact_id', 'meeting_id'] },
|
|
1095
|
+
{ name: 'assoc_contacts_notes', label: 'Contact ↔ Note', description: 'Associations between contacts and notes', apiPath: '/crm/v4/associations/contacts/notes', pkFields: ['contact_id', 'note_id'] },
|
|
1096
|
+
{ name: 'assoc_contacts_tasks', label: 'Contact ↔ Task', description: 'Associations between contacts and tasks', apiPath: '/crm/v4/associations/contacts/tasks', pkFields: ['contact_id', 'task_id'] },
|
|
1097
|
+
{ name: 'assoc_contacts_feedback_submissions', label: 'Contact ↔ Feedback Submission', description: 'Associations between contacts and feedback submissions', apiPath: '/crm/v4/associations/contacts/feedback_submissions', pkFields: ['contact_id', 'feedback_submission_id'] },
|
|
1098
|
+
{ name: 'assoc_companies_deals', label: 'Company ↔ Deal', description: 'Associations between companies and deals', apiPath: '/crm/v4/associations/companies/deals', pkFields: ['company_id', 'deal_id'] },
|
|
1099
|
+
{ name: 'assoc_companies_tickets', label: 'Company ↔ Ticket', description: 'Associations between companies and tickets', apiPath: '/crm/v4/associations/companies/tickets', pkFields: ['company_id', 'ticket_id'] },
|
|
1100
|
+
{ name: 'assoc_companies_calls', label: 'Company ↔ Call', description: 'Associations between companies and calls', apiPath: '/crm/v4/associations/companies/calls', pkFields: ['company_id', 'call_id'] },
|
|
1101
|
+
{ name: 'assoc_companies_emails', label: 'Company ↔ Email', description: 'Associations between companies and emails', apiPath: '/crm/v4/associations/companies/emails', pkFields: ['company_id', 'email_id'] },
|
|
1102
|
+
{ name: 'assoc_companies_meetings', label: 'Company ↔ Meeting', description: 'Associations between companies and meetings', apiPath: '/crm/v4/associations/companies/meetings', pkFields: ['company_id', 'meeting_id'] },
|
|
1103
|
+
{ name: 'assoc_companies_notes', label: 'Company ↔ Note', description: 'Associations between companies and notes', apiPath: '/crm/v4/associations/companies/notes', pkFields: ['company_id', 'note_id'] },
|
|
1104
|
+
{ name: 'assoc_companies_tasks', label: 'Company ↔ Task', description: 'Associations between companies and tasks', apiPath: '/crm/v4/associations/companies/tasks', pkFields: ['company_id', 'task_id'] },
|
|
1105
|
+
{ name: 'assoc_deals_calls', label: 'Deal ↔ Call', description: 'Associations between deals and calls', apiPath: '/crm/v4/associations/deals/calls', pkFields: ['deal_id', 'call_id'] },
|
|
1106
|
+
{ name: 'assoc_deals_emails', label: 'Deal ↔ Email', description: 'Associations between deals and emails', apiPath: '/crm/v4/associations/deals/emails', pkFields: ['deal_id', 'email_id'] },
|
|
1107
|
+
{ name: 'assoc_deals_meetings', label: 'Deal ↔ Meeting', description: 'Associations between deals and meetings', apiPath: '/crm/v4/associations/deals/meetings', pkFields: ['deal_id', 'meeting_id'] },
|
|
1108
|
+
{ name: 'assoc_deals_notes', label: 'Deal ↔ Note', description: 'Associations between deals and notes', apiPath: '/crm/v4/associations/deals/notes', pkFields: ['deal_id', 'note_id'] },
|
|
1109
|
+
{ name: 'assoc_deals_tasks', label: 'Deal ↔ Task', description: 'Associations between deals and tasks', apiPath: '/crm/v4/associations/deals/tasks', pkFields: ['deal_id', 'task_id'] },
|
|
1110
|
+
{ name: 'assoc_deals_quotes', label: 'Deal ↔ Quote', description: 'Associations between deals and quotes', apiPath: '/crm/v4/associations/deals/quotes', pkFields: ['deal_id', 'quote_id'] },
|
|
1111
|
+
{ name: 'assoc_deals_line_items', label: 'Deal ↔ Line Item', description: 'Associations between deals and line items', apiPath: '/crm/v4/associations/deals/line_items', pkFields: ['deal_id', 'line_item_id'] },
|
|
1112
|
+
{ name: 'assoc_tickets_calls', label: 'Ticket ↔ Call', description: 'Associations between tickets and calls', apiPath: '/crm/v4/associations/tickets/calls', pkFields: ['ticket_id', 'call_id'] },
|
|
1113
|
+
{ name: 'assoc_tickets_emails', label: 'Ticket ↔ Email', description: 'Associations between tickets and emails', apiPath: '/crm/v4/associations/tickets/emails', pkFields: ['ticket_id', 'email_id'] },
|
|
1114
|
+
{ name: 'assoc_tickets_meetings', label: 'Ticket ↔ Meeting', description: 'Associations between tickets and meetings', apiPath: '/crm/v4/associations/tickets/meetings', pkFields: ['ticket_id', 'meeting_id'] },
|
|
1115
|
+
{ name: 'assoc_tickets_notes', label: 'Ticket ↔ Note', description: 'Associations between tickets and notes', apiPath: '/crm/v4/associations/tickets/notes', pkFields: ['ticket_id', 'note_id'] },
|
|
1116
|
+
{ name: 'assoc_tickets_tasks', label: 'Ticket ↔ Task', description: 'Associations between tickets and tasks', apiPath: '/crm/v4/associations/tickets/tasks', pkFields: ['ticket_id', 'task_id'] },
|
|
1117
|
+
{ name: 'assoc_tickets_feedback_submissions', label: 'Ticket ↔ Feedback Submission', description: 'Associations between tickets and feedback submissions', apiPath: '/crm/v4/associations/tickets/feedback_submissions', pkFields: ['ticket_id', 'feedback_submission_id'] },
|
|
1118
|
+
{ name: 'assoc_quotes_contacts', label: 'Quote ↔ Contact', description: 'Associations between quotes and contacts', apiPath: '/crm/v4/associations/quotes/contacts', pkFields: ['quote_id', 'contact_id'] },
|
|
1119
|
+
{ name: 'assoc_quotes_line_items', label: 'Quote ↔ Line Item', description: 'Associations between quotes and line items', apiPath: '/crm/v4/associations/quotes/line_items', pkFields: ['quote_id', 'line_item_id'] },
|
|
1120
|
+
]; }
|
|
1121
|
+
/**
|
|
1122
|
+
* Discovers all HubSpot objects (standard + custom + non-CRM + associations) via live API and static lists.
|
|
1123
|
+
*/
|
|
1124
|
+
async DiscoverObjects(companyIntegration, contextUser) {
|
|
1125
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
1126
|
+
const headers = this.BuildHeaders(auth);
|
|
1127
|
+
const results = [];
|
|
1128
|
+
// Add all known standard objects
|
|
1129
|
+
for (const [typeId, name] of Object.entries(HubSpotConnector_1.STANDARD_OBJECTS)) {
|
|
1130
|
+
results.push({
|
|
1131
|
+
Name: name,
|
|
1132
|
+
Label: name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
|
|
1133
|
+
Description: `HubSpot ${name} (${typeId})`,
|
|
1134
|
+
SupportsIncrementalSync: true,
|
|
1135
|
+
SupportsWrite: true,
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
// Discover custom CRM objects via /crm/v3/schemas
|
|
1139
|
+
try {
|
|
1140
|
+
const schemasUrl = `${HUBSPOT_API_BASE}/crm/v3/schemas`;
|
|
1141
|
+
const schemasResp = await this.MakeHTTPRequest(auth, schemasUrl, 'GET', headers);
|
|
1142
|
+
if (schemasResp.Status === 200) {
|
|
1143
|
+
const body = schemasResp.Body;
|
|
1144
|
+
for (const s of body.results ?? []) {
|
|
1145
|
+
const name = s.name ?? s.labels?.singular ?? s.objectTypeId;
|
|
1146
|
+
if (!results.some(r => r.Name === name)) {
|
|
1147
|
+
results.push({
|
|
1148
|
+
Name: name,
|
|
1149
|
+
Label: s.labels?.singular ?? name,
|
|
1150
|
+
Description: `HubSpot custom object: ${name}`,
|
|
1151
|
+
SupportsIncrementalSync: true,
|
|
1152
|
+
SupportsWrite: true,
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
catch (err) {
|
|
1159
|
+
console.warn(`[HubSpot] Custom object discovery failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
1160
|
+
}
|
|
1161
|
+
// Add non-CRM objects (Marketing, CMS, Automation, etc.)
|
|
1162
|
+
for (const obj of HubSpotConnector_1.NON_CRM_OBJECTS) {
|
|
1163
|
+
if (!results.some(r => r.Name === obj.name)) {
|
|
1164
|
+
results.push({
|
|
1165
|
+
Name: obj.name,
|
|
1166
|
+
Label: obj.label,
|
|
1167
|
+
Description: obj.description,
|
|
1168
|
+
SupportsIncrementalSync: obj.incremental,
|
|
1169
|
+
SupportsWrite: obj.write,
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
// Add association objects (v4 API — fixed schema, no live field discovery)
|
|
1174
|
+
for (const assoc of HubSpotConnector_1.ASSOCIATION_OBJECTS) {
|
|
1175
|
+
if (!results.some(r => r.Name === assoc.name)) {
|
|
1176
|
+
results.push({
|
|
1177
|
+
Name: assoc.name,
|
|
1178
|
+
Label: assoc.label,
|
|
1179
|
+
Description: assoc.description,
|
|
1180
|
+
SupportsIncrementalSync: false,
|
|
1181
|
+
SupportsWrite: false,
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return results;
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Helper to check if an object name is a CRM object (has /crm/v3/properties endpoint).
|
|
1189
|
+
*/
|
|
1190
|
+
IsCRMObject(objectName) {
|
|
1191
|
+
const crmNames = new Set(Object.values(HubSpotConnector_1.STANDARD_OBJECTS));
|
|
1192
|
+
// Custom objects also use CRM properties API
|
|
1193
|
+
return crmNames.has(objectName) || objectName.startsWith('p_') || objectName.startsWith('2-');
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Finds the non-CRM object config by name.
|
|
1197
|
+
*/
|
|
1198
|
+
GetNonCRMObject(objectName) {
|
|
1199
|
+
return HubSpotConnector_1.NON_CRM_OBJECTS.find(o => o.name === objectName);
|
|
329
1200
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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;
|
|
1201
|
+
/**
|
|
1202
|
+
* Returns association object config if objectName is an association table, null otherwise.
|
|
1203
|
+
*/
|
|
1204
|
+
GetAssociationObject(objectName) {
|
|
1205
|
+
return HubSpotConnector_1.ASSOCIATION_OBJECTS.find(a => a.name === objectName);
|
|
344
1206
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
1207
|
+
/**
|
|
1208
|
+
* Discovers all fields on a HubSpot object via the Properties API.
|
|
1209
|
+
* Returns field types, constraints, PKs, and read-only flags from live metadata.
|
|
1210
|
+
*/
|
|
1211
|
+
async DiscoverFields(companyIntegration, objectName, contextUser) {
|
|
1212
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
1213
|
+
const headers = this.BuildHeaders(auth);
|
|
1214
|
+
// Non-CRM objects have fixed schemas — return static PK field; IntrospectSchema supplements from DB
|
|
1215
|
+
if (!this.IsCRMObject(objectName)) {
|
|
1216
|
+
return this.DiscoverNonCRMFields(objectName);
|
|
1217
|
+
}
|
|
1218
|
+
// CRM objects: live field discovery via Properties API
|
|
1219
|
+
const url = `${HUBSPOT_API_BASE}/crm/v3/properties/${objectName}`;
|
|
1220
|
+
const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
|
|
1221
|
+
if (response.Status !== 200)
|
|
1222
|
+
return [];
|
|
1223
|
+
const body = response.Body;
|
|
1224
|
+
const props = body.results ?? [];
|
|
1225
|
+
const fields = props
|
|
1226
|
+
.filter(p => !p.hidden)
|
|
1227
|
+
.map(p => ({
|
|
1228
|
+
Name: p.name,
|
|
1229
|
+
Label: p.label || p.name,
|
|
1230
|
+
Description: p.description || undefined,
|
|
1231
|
+
DataType: this.MapHubSpotType(p.type, p.fieldType),
|
|
1232
|
+
IsRequired: false,
|
|
1233
|
+
IsUniqueKey: false, // hasUniqueValue is HubSpot field-level uniqueness, not the record PK; only hs_object_id is the true unique key
|
|
1234
|
+
IsReadOnly: p.modificationMetadata?.readOnlyValue === true || p.calculated,
|
|
1235
|
+
}));
|
|
1236
|
+
// Ensure hs_object_id is the unique key — HubSpot's Properties API sets
|
|
1237
|
+
// hasUniqueValue=false for hs_object_id even though it IS the record identifier,
|
|
1238
|
+
// so we must override it regardless of what the API reports.
|
|
1239
|
+
const pkField = fields.find(f => f.Name === 'hs_object_id');
|
|
1240
|
+
if (pkField) {
|
|
1241
|
+
pkField.IsUniqueKey = true;
|
|
1242
|
+
pkField.IsReadOnly = true;
|
|
1243
|
+
}
|
|
1244
|
+
else {
|
|
1245
|
+
fields.push({
|
|
1246
|
+
Name: 'hs_object_id',
|
|
1247
|
+
Label: 'Object ID',
|
|
1248
|
+
Description: 'HubSpot internal object ID',
|
|
1249
|
+
DataType: 'string',
|
|
1250
|
+
IsRequired: true,
|
|
1251
|
+
IsUniqueKey: true,
|
|
1252
|
+
IsReadOnly: true,
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
return fields;
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Discovers fields for non-CRM objects by fetching the first page of results
|
|
1259
|
+
* and inferring field names/types from the response.
|
|
1260
|
+
*/
|
|
1261
|
+
/**
|
|
1262
|
+
* Non-CRM and association objects have fixed, documented schemas.
|
|
1263
|
+
* - Association objects: return both composite PK fields from ASSOCIATION_OBJECTS config.
|
|
1264
|
+
* - Non-CRM objects: return the PK field from NON_CRM_OBJECTS config.
|
|
1265
|
+
* IntrospectSchema's DB-fallback supplements with the full field list from metadata.
|
|
1266
|
+
* No live API sampling needed.
|
|
1267
|
+
*/
|
|
1268
|
+
DiscoverNonCRMFields(objectName) {
|
|
1269
|
+
const assocConfig = this.GetAssociationObject(objectName);
|
|
1270
|
+
if (assocConfig) {
|
|
1271
|
+
return assocConfig.pkFields.map(pk => ({
|
|
1272
|
+
Name: pk,
|
|
1273
|
+
Label: pk,
|
|
1274
|
+
Description: `Key field for ${assocConfig.label}`,
|
|
1275
|
+
DataType: 'string',
|
|
1276
|
+
IsRequired: true,
|
|
1277
|
+
IsUniqueKey: true,
|
|
1278
|
+
IsReadOnly: true,
|
|
1279
|
+
}));
|
|
1280
|
+
}
|
|
1281
|
+
const objConfig = this.GetNonCRMObject(objectName);
|
|
1282
|
+
if (!objConfig)
|
|
1283
|
+
return [];
|
|
1284
|
+
return [{
|
|
1285
|
+
Name: objConfig.pkField,
|
|
1286
|
+
Label: objConfig.pkField,
|
|
1287
|
+
Description: `Primary key for ${objConfig.label}`,
|
|
1288
|
+
DataType: 'string',
|
|
1289
|
+
IsRequired: true,
|
|
1290
|
+
IsUniqueKey: true,
|
|
1291
|
+
IsReadOnly: true,
|
|
1292
|
+
}];
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Full schema introspection — discovers all objects and their fields from the live API.
|
|
1296
|
+
*/
|
|
1297
|
+
async IntrospectSchema(companyIntegration, contextUser) {
|
|
1298
|
+
const objects = await this.DiscoverObjects(companyIntegration, contextUser);
|
|
1299
|
+
const result = { Objects: [] };
|
|
1300
|
+
for (const obj of objects) {
|
|
1301
|
+
try {
|
|
1302
|
+
const nonCrmConfig = this.GetNonCRMObject(obj.Name);
|
|
1303
|
+
const assocConfig = this.GetAssociationObject(obj.Name);
|
|
1304
|
+
const pkFieldNames = assocConfig
|
|
1305
|
+
? assocConfig.pkFields
|
|
1306
|
+
: [nonCrmConfig ? nonCrmConfig.pkField : 'hs_object_id'];
|
|
1307
|
+
let liveFields = await this.DiscoverFields(companyIntegration, obj.Name, contextUser);
|
|
1308
|
+
// Fall back to DB-cached Layer 1 static metadata when live discovery is insufficient:
|
|
1309
|
+
// - CRM objects: Properties API returned 0 fields (403/404/non-JSON)
|
|
1310
|
+
// - Non-CRM objects: endpoint returned only the pkField (parameterized or scope-restricted)
|
|
1311
|
+
const liveIsMinimal = liveFields.length === 0 ||
|
|
1312
|
+
(!this.IsCRMObject(obj.Name) && liveFields.length <= 1);
|
|
1313
|
+
if (liveIsMinimal) {
|
|
1314
|
+
try {
|
|
1315
|
+
const integrationObj = this.GetCachedObject(companyIntegration.IntegrationID, obj.Name);
|
|
1316
|
+
const dbFields = this.GetCachedFields(integrationObj.ID);
|
|
1317
|
+
if (dbFields.length > liveFields.length) {
|
|
1318
|
+
console.log(`[HubSpot] Live discovery returned ${liveFields.length} field(s) for "${obj.Name}" — using ${dbFields.length} DB-cached fields instead`);
|
|
1319
|
+
liveFields = dbFields.map(f => ({
|
|
1320
|
+
Name: f.Name,
|
|
1321
|
+
Label: f.DisplayName ?? f.Name,
|
|
1322
|
+
Description: f.Description ?? undefined,
|
|
1323
|
+
DataType: f.Type ?? 'string',
|
|
1324
|
+
IsRequired: f.IsRequired ?? false,
|
|
1325
|
+
IsUniqueKey: f.IsPrimaryKey || pkFieldNames.includes(f.Name),
|
|
1326
|
+
IsReadOnly: false,
|
|
1327
|
+
}));
|
|
1328
|
+
// Guarantee PK fields are present — DB records may not include them
|
|
1329
|
+
for (const pkName of pkFieldNames) {
|
|
1330
|
+
if (!liveFields.some(f => f.Name === pkName)) {
|
|
1331
|
+
liveFields.unshift({
|
|
1332
|
+
Name: pkName,
|
|
1333
|
+
Label: pkName,
|
|
1334
|
+
Description: `Primary key for ${obj.Name}`,
|
|
1335
|
+
DataType: 'string',
|
|
1336
|
+
IsRequired: true,
|
|
1337
|
+
IsUniqueKey: true,
|
|
1338
|
+
IsReadOnly: true,
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
catch {
|
|
1345
|
+
// No DB record yet — proceed with whatever live returned
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
const sourceFields = liveFields.map(f => ({
|
|
1349
|
+
Name: f.Name,
|
|
1350
|
+
Label: f.Label,
|
|
1351
|
+
Description: f.Description,
|
|
1352
|
+
SourceType: f.DataType,
|
|
1353
|
+
IsRequired: f.IsRequired,
|
|
1354
|
+
IsPrimaryKey: f.IsUniqueKey || pkFieldNames.includes(f.Name),
|
|
1355
|
+
IsForeignKey: false,
|
|
1356
|
+
ForeignKeyTarget: null,
|
|
1357
|
+
MaxLength: null,
|
|
1358
|
+
Precision: null,
|
|
1359
|
+
Scale: null,
|
|
1360
|
+
DefaultValue: null,
|
|
1361
|
+
}));
|
|
1362
|
+
const pkFields = sourceFields.filter(f => f.IsPrimaryKey);
|
|
1363
|
+
result.Objects.push({
|
|
1364
|
+
ExternalName: obj.Name,
|
|
1365
|
+
ExternalLabel: obj.Label ?? obj.Name,
|
|
1366
|
+
Description: obj.Description,
|
|
1367
|
+
Fields: sourceFields,
|
|
1368
|
+
PrimaryKeyFields: pkFields.map(f => f.Name),
|
|
1369
|
+
Relationships: [],
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
catch (err) {
|
|
1373
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1374
|
+
console.warn(`[HubSpot] Live discovery threw for "${obj.Name}": ${msg} — trying DB-cached fields`);
|
|
1375
|
+
// Unexpected exception path — try DB as last resort
|
|
1376
|
+
try {
|
|
1377
|
+
const nonCrmConfig2 = this.GetNonCRMObject(obj.Name);
|
|
1378
|
+
const assocConfig2 = this.GetAssociationObject(obj.Name);
|
|
1379
|
+
const pkFieldNames2 = assocConfig2
|
|
1380
|
+
? assocConfig2.pkFields
|
|
1381
|
+
: [nonCrmConfig2 ? nonCrmConfig2.pkField : 'hs_object_id'];
|
|
1382
|
+
const integrationObj = this.GetCachedObject(companyIntegration.IntegrationID, obj.Name);
|
|
1383
|
+
const dbFields = this.GetCachedFields(integrationObj.ID);
|
|
1384
|
+
if (dbFields.length > 0) {
|
|
1385
|
+
const sourceFields = dbFields.map(f => ({
|
|
1386
|
+
Name: f.Name,
|
|
1387
|
+
Label: f.DisplayName ?? f.Name,
|
|
1388
|
+
Description: f.Description ?? undefined,
|
|
1389
|
+
SourceType: f.Type ?? 'string',
|
|
1390
|
+
IsRequired: f.IsRequired ?? false,
|
|
1391
|
+
IsPrimaryKey: f.IsPrimaryKey || pkFieldNames2.includes(f.Name),
|
|
1392
|
+
IsForeignKey: false,
|
|
1393
|
+
ForeignKeyTarget: null,
|
|
1394
|
+
MaxLength: null,
|
|
1395
|
+
Precision: null,
|
|
1396
|
+
Scale: null,
|
|
1397
|
+
DefaultValue: null,
|
|
1398
|
+
}));
|
|
1399
|
+
const pkFields = sourceFields.filter(f => f.IsPrimaryKey);
|
|
1400
|
+
result.Objects.push({
|
|
1401
|
+
ExternalName: obj.Name,
|
|
1402
|
+
ExternalLabel: obj.Label ?? obj.Name,
|
|
1403
|
+
Description: obj.Description,
|
|
1404
|
+
Fields: sourceFields,
|
|
1405
|
+
PrimaryKeyFields: pkFields.map(f => f.Name),
|
|
1406
|
+
Relationships: [],
|
|
1407
|
+
});
|
|
1408
|
+
console.log(`[HubSpot] Used ${dbFields.length} DB-cached fields for "${obj.Name}" after exception`);
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
catch {
|
|
1413
|
+
// DB fallback also failed — truly skip this object
|
|
1414
|
+
}
|
|
1415
|
+
console.warn(`[HubSpot] Skipping "${obj.Name}" — no live or DB fields available`);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return result;
|
|
352
1419
|
}
|
|
353
1420
|
// ─── CRUD Operations ─────────────────────────────────────────────────
|
|
354
1421
|
/**
|
|
@@ -370,8 +1437,13 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
|
|
|
370
1437
|
}
|
|
371
1438
|
/**
|
|
372
1439
|
* Creates a new record in HubSpot.
|
|
1440
|
+
* Routes association objects to the v4 batch/create endpoint instead of v3 objects.
|
|
373
1441
|
*/
|
|
374
1442
|
async CreateRecord(ctx) {
|
|
1443
|
+
const assocConfig = this.GetAssociationObject(ctx.ObjectName);
|
|
1444
|
+
if (assocConfig) {
|
|
1445
|
+
return this.CreateAssociation(ctx.CompanyIntegration, ctx.ContextUser, ctx.ObjectName, ctx.Attributes, assocConfig);
|
|
1446
|
+
}
|
|
375
1447
|
const companyIntegration = ctx.CompanyIntegration;
|
|
376
1448
|
const contextUser = ctx.ContextUser;
|
|
377
1449
|
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
@@ -391,8 +1463,15 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
|
|
|
391
1463
|
}
|
|
392
1464
|
/**
|
|
393
1465
|
* Updates an existing record in HubSpot by ExternalID.
|
|
1466
|
+
* For association objects, re-creates the association (idempotent in HubSpot).
|
|
394
1467
|
*/
|
|
395
1468
|
async UpdateRecord(ctx) {
|
|
1469
|
+
const assocConfig = this.GetAssociationObject(ctx.ObjectName);
|
|
1470
|
+
if (assocConfig) {
|
|
1471
|
+
// Associations have no updatable properties — the IDs ARE the relationship.
|
|
1472
|
+
// Re-creating is idempotent: HubSpot ignores duplicates.
|
|
1473
|
+
return this.CreateAssociation(ctx.CompanyIntegration, ctx.ContextUser, ctx.ObjectName, ctx.Attributes, assocConfig);
|
|
1474
|
+
}
|
|
396
1475
|
const companyIntegration = ctx.CompanyIntegration;
|
|
397
1476
|
const contextUser = ctx.ContextUser;
|
|
398
1477
|
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
@@ -412,8 +1491,13 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
|
|
|
412
1491
|
}
|
|
413
1492
|
/**
|
|
414
1493
|
* Deletes (archives) a record in HubSpot by ExternalID.
|
|
1494
|
+
* Routes association objects to the v4 batch/archive endpoint instead of v3 objects.
|
|
415
1495
|
*/
|
|
416
1496
|
async DeleteRecord(ctx) {
|
|
1497
|
+
const assocConfig = this.GetAssociationObject(ctx.ObjectName);
|
|
1498
|
+
if (assocConfig) {
|
|
1499
|
+
return this.DeleteAssociation(ctx.CompanyIntegration, ctx.ContextUser, ctx.ObjectName, ctx.ExternalID, assocConfig);
|
|
1500
|
+
}
|
|
417
1501
|
const companyIntegration = ctx.CompanyIntegration;
|
|
418
1502
|
const contextUser = ctx.ContextUser;
|
|
419
1503
|
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
@@ -429,6 +1513,69 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
|
|
|
429
1513
|
}
|
|
430
1514
|
return this.BuildCRUDErrorResult(response, 'DeleteRecord', ctx.ObjectName);
|
|
431
1515
|
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Creates an association in HubSpot using the v4 batch/create endpoint.
|
|
1518
|
+
* ExternalID returned is "{leftID}|{rightID}" matching the pull ExternalID format.
|
|
1519
|
+
*/
|
|
1520
|
+
async CreateAssociation(rawCompanyIntegration, rawContextUser, objectName, attributes, assocConfig) {
|
|
1521
|
+
const companyIntegration = rawCompanyIntegration;
|
|
1522
|
+
const contextUser = rawContextUser;
|
|
1523
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
1524
|
+
const headers = this.BuildHeaders(auth);
|
|
1525
|
+
const [leftField, rightField] = assocConfig.pkFields;
|
|
1526
|
+
const leftID = String(attributes[leftField] ?? '');
|
|
1527
|
+
const rightID = String(attributes[rightField] ?? '');
|
|
1528
|
+
if (!leftID || !rightID) {
|
|
1529
|
+
return {
|
|
1530
|
+
Success: false,
|
|
1531
|
+
ExternalID: '',
|
|
1532
|
+
StatusCode: 400,
|
|
1533
|
+
ErrorMessage: `CreateAssociation ${objectName}: missing PK fields '${leftField}' or '${rightField}' in attributes`,
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
const pathParts = assocConfig.apiPath.split('/').filter(Boolean);
|
|
1537
|
+
const fromType = pathParts[pathParts.length - 2];
|
|
1538
|
+
const toType = pathParts[pathParts.length - 1];
|
|
1539
|
+
const url = `${HUBSPOT_API_BASE}/crm/v4/associations/${fromType}/${toType}/batch/create`;
|
|
1540
|
+
const body = { inputs: [{ from: { id: leftID }, to: { id: rightID }, types: [] }] };
|
|
1541
|
+
const response = await this.MakeHTTPRequest(auth, url, 'POST', headers, body);
|
|
1542
|
+
if (response.Status >= 200 && response.Status < 300) {
|
|
1543
|
+
return { Success: true, ExternalID: `${leftID}|${rightID}`, StatusCode: response.Status };
|
|
1544
|
+
}
|
|
1545
|
+
return this.BuildCRUDErrorResult(response, 'CreateAssociation', objectName);
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Removes an association in HubSpot using the v4 batch/archive endpoint.
|
|
1549
|
+
* ExternalID must be "{leftID}|{rightID}" — the same format stored by pull sync.
|
|
1550
|
+
*/
|
|
1551
|
+
async DeleteAssociation(rawCompanyIntegration, rawContextUser, objectName, externalID, assocConfig) {
|
|
1552
|
+
const companyIntegration = rawCompanyIntegration;
|
|
1553
|
+
const contextUser = rawContextUser;
|
|
1554
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
1555
|
+
const headers = this.BuildHeaders(auth);
|
|
1556
|
+
const pipeIndex = externalID.indexOf('|');
|
|
1557
|
+
if (pipeIndex < 0) {
|
|
1558
|
+
return {
|
|
1559
|
+
Success: false,
|
|
1560
|
+
ExternalID: externalID,
|
|
1561
|
+
StatusCode: 400,
|
|
1562
|
+
ErrorMessage: `DeleteAssociation ${objectName}: cannot parse composite ExternalID '${externalID}' — expected 'leftID|rightID'`,
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
const leftID = externalID.substring(0, pipeIndex);
|
|
1566
|
+
const rightID = externalID.substring(pipeIndex + 1);
|
|
1567
|
+
const pathParts = assocConfig.apiPath.split('/').filter(Boolean);
|
|
1568
|
+
const fromType = pathParts[pathParts.length - 2];
|
|
1569
|
+
const toType = pathParts[pathParts.length - 1];
|
|
1570
|
+
const url = `${HUBSPOT_API_BASE}/crm/v4/associations/${fromType}/${toType}/batch/archive`;
|
|
1571
|
+
const body = { inputs: [{ from: { id: leftID }, to: { id: rightID } }] };
|
|
1572
|
+
const response = await this.MakeHTTPRequest(auth, url, 'POST', headers, body);
|
|
1573
|
+
// batch/archive returns 204 No Content on success
|
|
1574
|
+
if (response.Status === 204 || (response.Status >= 200 && response.Status < 300)) {
|
|
1575
|
+
return { Success: true, ExternalID: externalID, StatusCode: response.Status };
|
|
1576
|
+
}
|
|
1577
|
+
return this.BuildCRUDErrorResult(response, 'DeleteAssociation', objectName);
|
|
1578
|
+
}
|
|
432
1579
|
/**
|
|
433
1580
|
* Searches HubSpot objects using the CRM search API.
|
|
434
1581
|
*/
|
|
@@ -511,9 +1658,16 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
|
|
|
511
1658
|
/** Builds a CRUDResult for error responses. */
|
|
512
1659
|
BuildCRUDErrorResult(response, operation, objectName) {
|
|
513
1660
|
const bodyObj = response.Body;
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
1661
|
+
let message;
|
|
1662
|
+
if (response.Status === 403) {
|
|
1663
|
+
message = `[HubSpot] 403 Forbidden — OAuth scope missing for ${operation} on '${objectName}'. ` +
|
|
1664
|
+
`Add the required scope to your HubSpot app and reconnect.`;
|
|
1665
|
+
}
|
|
1666
|
+
else {
|
|
1667
|
+
message = bodyObj?.['message']
|
|
1668
|
+
? String(bodyObj['message'])
|
|
1669
|
+
: `[HubSpot] ${operation} on ${objectName} failed (HTTP ${response.Status})`;
|
|
1670
|
+
}
|
|
517
1671
|
return {
|
|
518
1672
|
Success: false,
|
|
519
1673
|
ErrorMessage: message,
|
|
@@ -522,6 +1676,8 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
|
|
|
522
1676
|
}
|
|
523
1677
|
// ─── Abstract method implementations (BaseRESTIntegrationConnector) ──
|
|
524
1678
|
async Authenticate(companyIntegration, contextUser) {
|
|
1679
|
+
if (this._cachedAuth)
|
|
1680
|
+
return this._cachedAuth;
|
|
525
1681
|
console.log(`[HubSpot] Authenticating...`);
|
|
526
1682
|
const credentials = await this.LoadCredentials(companyIntegration, contextUser);
|
|
527
1683
|
const config = this.BuildConnectionConfig(credentials, companyIntegration);
|
|
@@ -532,6 +1688,7 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
|
|
|
532
1688
|
Credentials: credentials,
|
|
533
1689
|
Config: config,
|
|
534
1690
|
};
|
|
1691
|
+
this._cachedAuth = auth;
|
|
535
1692
|
return auth;
|
|
536
1693
|
}
|
|
537
1694
|
BuildHeaders(auth) {
|
|
@@ -562,7 +1719,14 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
|
|
|
562
1719
|
if (response.status === 204) {
|
|
563
1720
|
return this.BuildRESTResponse(response, {});
|
|
564
1721
|
}
|
|
565
|
-
|
|
1722
|
+
let responseBody;
|
|
1723
|
+
try {
|
|
1724
|
+
responseBody = await response.json();
|
|
1725
|
+
}
|
|
1726
|
+
catch {
|
|
1727
|
+
// Non-JSON body (e.g. HTML error page) — treat as empty object
|
|
1728
|
+
responseBody = {};
|
|
1729
|
+
}
|
|
566
1730
|
return this.BuildRESTResponse(response, responseBody);
|
|
567
1731
|
}
|
|
568
1732
|
throw new Error(`HubSpot API request failed after ${maxRetries} retries: ${url}`);
|
|
@@ -710,6 +1874,12 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
|
|
|
710
1874
|
return 'boolean';
|
|
711
1875
|
case 'enumeration':
|
|
712
1876
|
return 'enum';
|
|
1877
|
+
case 'json':
|
|
1878
|
+
// HubSpot json-type properties can be arbitrarily large objects.
|
|
1879
|
+
// Map to 'text' so the schema builder creates nvarchar(MAX) columns.
|
|
1880
|
+
return 'text';
|
|
1881
|
+
case 'phone_number':
|
|
1882
|
+
return 'string';
|
|
713
1883
|
default:
|
|
714
1884
|
return type;
|
|
715
1885
|
}
|
|
@@ -724,7 +1894,7 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
|
|
|
724
1894
|
AccessToken: credentials.AccessToken,
|
|
725
1895
|
ApiVersion: credentials.ApiVersion,
|
|
726
1896
|
};
|
|
727
|
-
const configJson = companyIntegration.
|
|
1897
|
+
const configJson = companyIntegration.Configuration;
|
|
728
1898
|
if (configJson) {
|
|
729
1899
|
this.ApplyConfigOverrides(config, configJson);
|
|
730
1900
|
}
|
|
@@ -759,14 +1929,14 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
|
|
|
759
1929
|
*/
|
|
760
1930
|
async LoadCredentials(companyIntegration, contextUser) {
|
|
761
1931
|
// Try loading from linked Credential entity first
|
|
762
|
-
const credentialID = companyIntegration.
|
|
1932
|
+
const credentialID = companyIntegration.CredentialID;
|
|
763
1933
|
if (credentialID) {
|
|
764
1934
|
const creds = await this.LoadFromCredentialEntity(credentialID, contextUser);
|
|
765
1935
|
if (creds)
|
|
766
1936
|
return creds;
|
|
767
1937
|
}
|
|
768
1938
|
// Fallback: read from CompanyIntegration Configuration JSON
|
|
769
|
-
const configJson = companyIntegration.
|
|
1939
|
+
const configJson = companyIntegration.Configuration;
|
|
770
1940
|
if (configJson) {
|
|
771
1941
|
const creds = this.ParseCredentialJson(configJson);
|
|
772
1942
|
if (creds)
|
|
@@ -828,40 +1998,455 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
|
|
|
828
1998
|
}
|
|
829
1999
|
// ─── Association fetch (v4 API) ───────────────────────────────────
|
|
830
2000
|
/**
|
|
831
|
-
* Overrides FetchChanges to
|
|
832
|
-
*
|
|
833
|
-
*
|
|
2001
|
+
* Overrides FetchChanges to support three fetch strategies:
|
|
2002
|
+
*
|
|
2003
|
+
* 1. **Association objects** → v4 per-object associations endpoint
|
|
2004
|
+
* 2. **Incremental sync** (watermark set) → HubSpot search API with server-side
|
|
2005
|
+
* `hs_lastmodifieddate >= watermark` filter
|
|
2006
|
+
* 3. **Full load** (no watermark / first sync) → standard list API via base class
|
|
834
2007
|
*/
|
|
835
2008
|
async FetchChanges(ctx) {
|
|
836
2009
|
const obj = this.GetCachedObject(ctx.CompanyIntegration.IntegrationID, ctx.ObjectName);
|
|
837
2010
|
if (obj.Category === 'Association') {
|
|
838
2011
|
return this.FetchAssociationChanges(ctx, obj);
|
|
839
2012
|
}
|
|
840
|
-
|
|
841
|
-
//
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
2013
|
+
// Non-CRM objects (Marketing, CMS, etc.) don't support the CRM search API.
|
|
2014
|
+
// Use the base class list endpoint (reads APIPath from IntegrationObject) with
|
|
2015
|
+
// client-side watermark filtering.
|
|
2016
|
+
const nonCrm = this.GetNonCRMObject(ctx.ObjectName);
|
|
2017
|
+
if (nonCrm) {
|
|
2018
|
+
return this.FetchNonCRMChanges(ctx, nonCrm);
|
|
2019
|
+
}
|
|
2020
|
+
// CRM objects: use search-based incremental sync when a watermark exists
|
|
2021
|
+
if (ctx.WatermarkValue) {
|
|
2022
|
+
return this.FetchChangesViaSearch(ctx);
|
|
2023
|
+
}
|
|
2024
|
+
// First sync (no watermark): use list API for full load.
|
|
2025
|
+
// Cannot use super.FetchChanges here — the base class reads raw HubSpot records
|
|
2026
|
+
// without flattening the nested {id, properties: {...}} envelope, so ExternalID
|
|
2027
|
+
// would resolve to '' (raw['hs_object_id'] is undefined at the top level).
|
|
2028
|
+
return this.FetchCRMFullLoad(ctx);
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Full-load path for CRM objects (first sync, no watermark).
|
|
2032
|
+
* Uses the CRM list endpoint with property expansion and FlattenHubSpotRecord so that
|
|
2033
|
+
* ExternalID is correctly built from hs_object_id (mirroring FetchChangesViaSearch).
|
|
2034
|
+
* The base class FetchChanges cannot be used here because it reads raw[field] without
|
|
2035
|
+
* flattening, causing ExternalID="" for every record.
|
|
2036
|
+
*/
|
|
2037
|
+
async FetchCRMFullLoad(ctx) {
|
|
2038
|
+
const companyIntegration = ctx.CompanyIntegration;
|
|
2039
|
+
const contextUser = ctx.ContextUser;
|
|
2040
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
2041
|
+
const headers = this.BuildHeaders(auth);
|
|
2042
|
+
const limit = Math.min(ctx.BatchSize ?? 100, 100); // HubSpot CRM list API max is 100
|
|
2043
|
+
const propertiesParam = this.BuildPropertiesParam(ctx.ObjectName, ctx.RequestedSourceFields);
|
|
2044
|
+
let url = `${HUBSPOT_API_BASE}/crm/v3/objects/${ctx.ObjectName}?limit=${limit}${propertiesParam}`;
|
|
2045
|
+
if (ctx.CurrentCursor) {
|
|
2046
|
+
url += `&after=${ctx.CurrentCursor}`;
|
|
2047
|
+
}
|
|
2048
|
+
const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
|
|
2049
|
+
if (response.Status !== 200) {
|
|
2050
|
+
const body = response.Body;
|
|
2051
|
+
const msg = body?.message ?? body?.error ?? JSON.stringify(body);
|
|
2052
|
+
console.warn(`[HubSpot] CRM full-load failed for ${ctx.ObjectName}: HTTP ${response.Status} — ${msg}`);
|
|
2053
|
+
return { Records: [], HasMore: false };
|
|
2054
|
+
}
|
|
2055
|
+
const body = response.Body;
|
|
2056
|
+
const rawResults = body.results ?? [];
|
|
2057
|
+
const records = rawResults.map(r => {
|
|
2058
|
+
const raw = r;
|
|
2059
|
+
const flat = this.FlattenHubSpotRecord(raw);
|
|
2060
|
+
return {
|
|
2061
|
+
ExternalID: String(flat['hs_object_id'] ?? raw['id'] ?? ''),
|
|
2062
|
+
ObjectType: ctx.ObjectName,
|
|
2063
|
+
Fields: flat,
|
|
2064
|
+
};
|
|
2065
|
+
});
|
|
2066
|
+
const nextCursor = body.paging?.next?.after;
|
|
2067
|
+
const hasMore = nextCursor != null;
|
|
2068
|
+
let newWatermark;
|
|
2069
|
+
if (!hasMore) {
|
|
2070
|
+
const dateField = this.GetWatermarkField(ctx.ObjectName);
|
|
2071
|
+
const latest = this.FindLatestDate(records, dateField);
|
|
849
2072
|
if (latest)
|
|
850
|
-
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
2073
|
+
newWatermark = latest;
|
|
2074
|
+
}
|
|
2075
|
+
return {
|
|
2076
|
+
Records: records,
|
|
2077
|
+
HasMore: hasMore,
|
|
2078
|
+
NextCursor: nextCursor,
|
|
2079
|
+
NewWatermarkValue: newWatermark,
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
/**
|
|
2083
|
+
* Fetches all pages from a single non-CRM API endpoint URL, accumulating all records.
|
|
2084
|
+
* Used as a building block for both flat and parameterized endpoint fetches.
|
|
2085
|
+
*/
|
|
2086
|
+
async FetchAllPagesFromURL(auth, baseUrl, pkField, objectName) {
|
|
2087
|
+
const headers = this.BuildHeaders(auth);
|
|
2088
|
+
const allRecords = [];
|
|
2089
|
+
let cursor;
|
|
2090
|
+
do {
|
|
2091
|
+
const url = cursor ? `${baseUrl}&after=${encodeURIComponent(cursor)}` : baseUrl;
|
|
2092
|
+
const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
|
|
2093
|
+
if (response.Status !== 200) {
|
|
2094
|
+
const body = response.Body;
|
|
2095
|
+
const msg = body?.message ?? body?.error ?? JSON.stringify(body);
|
|
2096
|
+
console.warn(`[HubSpot] FetchAllPages failed for ${objectName}: HTTP ${response.Status} — ${msg}`);
|
|
2097
|
+
break;
|
|
2098
|
+
}
|
|
2099
|
+
const body = response.Body;
|
|
2100
|
+
const results = (body['results'] ?? body['objects'] ?? body['messages'] ?? []);
|
|
2101
|
+
for (const raw of results) {
|
|
2102
|
+
const id = String(raw[pkField] ?? raw['id'] ?? '');
|
|
2103
|
+
const properties = raw['properties'];
|
|
2104
|
+
const fields = properties ? { ...raw, ...properties } : raw;
|
|
2105
|
+
allRecords.push({ ExternalID: id, ObjectType: objectName, Fields: fields });
|
|
2106
|
+
}
|
|
2107
|
+
// Only follow pagination cursor when results were returned (avoid infinite loop)
|
|
2108
|
+
const paging = body['paging'];
|
|
2109
|
+
cursor = results.length > 0 ? paging?.next?.after : undefined;
|
|
2110
|
+
} while (cursor);
|
|
2111
|
+
return allRecords;
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Handles parameterized endpoints (apiPath with {placeholder}) by fan-out:
|
|
2115
|
+
* fetches all parent records, then fetches children for each parent ID.
|
|
2116
|
+
* All child records are accumulated and returned as a single batch (HasMore: false).
|
|
2117
|
+
*
|
|
2118
|
+
* Skips objects where parentObject is not found in NON_CRM_OBJECTS (config error).
|
|
2119
|
+
* Skips objects with {appId} placeholder — these require Developer App configuration.
|
|
2120
|
+
*/
|
|
2121
|
+
async FetchParameterizedChanges(ctx, objConfig) {
|
|
2122
|
+
// {appId} placeholders require developer app config — not available at runtime
|
|
2123
|
+
if (objConfig.apiPath.includes('{appId}')) {
|
|
2124
|
+
console.warn(`[HubSpot] ${ctx.ObjectName}: Parameterized endpoint requires {appId} (Developer App ID). ` +
|
|
2125
|
+
`Configure AppID in connector settings to enable this object. Returning empty batch.`);
|
|
2126
|
+
return { Records: [], HasMore: false };
|
|
2127
|
+
}
|
|
2128
|
+
const companyIntegration = ctx.CompanyIntegration;
|
|
2129
|
+
const contextUser = ctx.ContextUser;
|
|
2130
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
2131
|
+
// Find and validate parent config
|
|
2132
|
+
const parentConfig = HubSpotConnector_1.NON_CRM_OBJECTS.find(o => o.name === objConfig.parentObject);
|
|
2133
|
+
if (!parentConfig) {
|
|
2134
|
+
console.warn(`[HubSpot] ${ctx.ObjectName}: parentObject '${objConfig.parentObject}' not found in NON_CRM_OBJECTS`);
|
|
2135
|
+
return { Records: [], HasMore: false };
|
|
2136
|
+
}
|
|
2137
|
+
// Fetch all parent records
|
|
2138
|
+
const parentUrl = `${HUBSPOT_API_BASE}${parentConfig.apiPath}?limit=100`;
|
|
2139
|
+
const parentRecords = await this.FetchAllPagesFromURL(auth, parentUrl, parentConfig.pkField, parentConfig.name);
|
|
2140
|
+
if (parentRecords.length === 0) {
|
|
2141
|
+
return { Records: [], HasMore: false };
|
|
2142
|
+
}
|
|
2143
|
+
// Extract the placeholder name from the apiPath (e.g. '{pipelineId}' → 'pipelineId')
|
|
2144
|
+
const placeholderMatch = objConfig.apiPath.match(/\{([^}]+)\}/);
|
|
2145
|
+
if (!placeholderMatch) {
|
|
2146
|
+
return { Records: [], HasMore: false };
|
|
2147
|
+
}
|
|
2148
|
+
// Fan-out: fetch children for each parent
|
|
2149
|
+
const allChildren = [];
|
|
2150
|
+
for (const parent of parentRecords) {
|
|
2151
|
+
const parentId = String(parent.Fields[parentConfig.pkField] ?? parent.ExternalID ?? '');
|
|
2152
|
+
if (!parentId)
|
|
2153
|
+
continue;
|
|
2154
|
+
// Substitute placeholder with actual parent ID
|
|
2155
|
+
const childPath = objConfig.apiPath.replace(`{${placeholderMatch[1]}}`, encodeURIComponent(parentId));
|
|
2156
|
+
const childUrl = `${HUBSPOT_API_BASE}${childPath}?limit=100`;
|
|
2157
|
+
const children = await this.FetchAllPagesFromURL(auth, childUrl, objConfig.pkField, ctx.ObjectName);
|
|
2158
|
+
// Tag each child with its parent ID so the record is traceable
|
|
2159
|
+
for (const child of children) {
|
|
2160
|
+
child.Fields[`_parent_${parentConfig.pkField}`] = parentId;
|
|
2161
|
+
allChildren.push(child);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
// Apply watermark filtering client-side if watermark is set
|
|
2165
|
+
let filtered = allChildren;
|
|
2166
|
+
if (ctx.WatermarkValue) {
|
|
2167
|
+
const watermarkMs = new Date(ctx.WatermarkValue).getTime();
|
|
2168
|
+
filtered = allChildren.filter(r => {
|
|
2169
|
+
for (const key of ['updatedAt', 'updated', 'createdAt', 'hs_lastmodifieddate']) {
|
|
2170
|
+
const val = r.Fields[key];
|
|
2171
|
+
if (val && typeof val === 'string') {
|
|
2172
|
+
const ms = new Date(val).getTime();
|
|
2173
|
+
if (!isNaN(ms) && ms > watermarkMs)
|
|
2174
|
+
return true;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
return false;
|
|
862
2178
|
});
|
|
863
2179
|
}
|
|
864
|
-
|
|
2180
|
+
const newWatermark = this.FindLatestDateInFields(filtered);
|
|
2181
|
+
return {
|
|
2182
|
+
Records: filtered,
|
|
2183
|
+
HasMore: false,
|
|
2184
|
+
NewWatermarkValue: newWatermark,
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
/**
|
|
2188
|
+
* Fetches records from non-CRM HubSpot APIs (Marketing, CMS, Files, etc.).
|
|
2189
|
+
* These endpoints use standard REST list pagination, not the CRM search API.
|
|
2190
|
+
* Watermark filtering is client-side based on date fields in the response.
|
|
2191
|
+
*
|
|
2192
|
+
* Dispatches to FetchParameterizedChanges when apiPath contains {placeholder}.
|
|
2193
|
+
*/
|
|
2194
|
+
async FetchNonCRMChanges(ctx, objConfig) {
|
|
2195
|
+
// Parameterized endpoints (apiPath with {placeholder}) require fan-out across parent IDs.
|
|
2196
|
+
// Dispatch to the specialized handler instead of building a URL with a literal placeholder.
|
|
2197
|
+
if (objConfig.parentObject) {
|
|
2198
|
+
return this.FetchParameterizedChanges(ctx, objConfig);
|
|
2199
|
+
}
|
|
2200
|
+
const companyIntegration = ctx.CompanyIntegration;
|
|
2201
|
+
const contextUser = ctx.ContextUser;
|
|
2202
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
2203
|
+
const headers = this.BuildHeaders(auth);
|
|
2204
|
+
// Build URL with pagination — varies by endpoint type
|
|
2205
|
+
const limit = ctx.BatchSize || 100;
|
|
2206
|
+
let url;
|
|
2207
|
+
let isScimPagination = false;
|
|
2208
|
+
if (objConfig.incrementalParam === 'startIndex') {
|
|
2209
|
+
// SCIM 2.0 endpoints (scim_users, scim_groups) use offset-based pagination:
|
|
2210
|
+
// startIndex is 1-based, count is page size. CurrentCursor stores next startIndex as string.
|
|
2211
|
+
isScimPagination = true;
|
|
2212
|
+
const startIndex = ctx.CurrentCursor ? parseInt(ctx.CurrentCursor, 10) : 1;
|
|
2213
|
+
url = `${HUBSPOT_API_BASE}${objConfig.apiPath}?startIndex=${startIndex}&count=${limit}`;
|
|
2214
|
+
}
|
|
2215
|
+
else {
|
|
2216
|
+
url = `${HUBSPOT_API_BASE}${objConfig.apiPath}?limit=${limit}`;
|
|
2217
|
+
if (ctx.CurrentCursor) {
|
|
2218
|
+
url += `&after=${ctx.CurrentCursor}`;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
// Server-side incremental filtering (preferred over client-side)
|
|
2222
|
+
if (ctx.WatermarkValue && objConfig.serverIncrementalParam) {
|
|
2223
|
+
url += `&${objConfig.serverIncrementalParam}=${encodeURIComponent(ctx.WatermarkValue)}`;
|
|
2224
|
+
}
|
|
2225
|
+
else if (ctx.WatermarkValue && objConfig.incrementalParam === 'after') {
|
|
2226
|
+
// Legacy: owners endpoint uses incrementalParam: 'after' → maps to updatedAfter
|
|
2227
|
+
url += `&updatedAfter=${ctx.WatermarkValue}`;
|
|
2228
|
+
}
|
|
2229
|
+
const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
|
|
2230
|
+
if (response.Status !== 200) {
|
|
2231
|
+
const body = response.Body;
|
|
2232
|
+
const msg = body?.message ?? body?.error ?? JSON.stringify(body);
|
|
2233
|
+
console.warn(`[HubSpot] Non-CRM fetch failed for ${ctx.ObjectName}: HTTP ${response.Status} — ${msg}`);
|
|
2234
|
+
return { Records: [], HasMore: false };
|
|
2235
|
+
}
|
|
2236
|
+
const body = response.Body;
|
|
2237
|
+
// SCIM responses use 'Resources' array; standard HubSpot uses 'results' or 'objects'
|
|
2238
|
+
const results = (body['Resources'] ?? body['results'] ?? body['objects'] ?? []);
|
|
2239
|
+
// Build ExternalRecords
|
|
2240
|
+
const records = results.map(raw => {
|
|
2241
|
+
const id = String(raw[objConfig.pkField] ?? raw['id'] ?? '');
|
|
2242
|
+
// Flatten properties if they exist (some endpoints nest under 'properties')
|
|
2243
|
+
const properties = raw['properties'];
|
|
2244
|
+
const fields = properties ? { ...raw, ...properties } : raw;
|
|
2245
|
+
return {
|
|
2246
|
+
ExternalID: id,
|
|
2247
|
+
ObjectType: ctx.ObjectName,
|
|
2248
|
+
Fields: fields,
|
|
2249
|
+
};
|
|
2250
|
+
});
|
|
2251
|
+
// Client-side watermark filtering only when no server-side param is available
|
|
2252
|
+
const hasServerSideFilter = !!(ctx.WatermarkValue && (objConfig.serverIncrementalParam || objConfig.incrementalParam === 'after'));
|
|
2253
|
+
let filteredRecords = records;
|
|
2254
|
+
if (ctx.WatermarkValue && !hasServerSideFilter) {
|
|
2255
|
+
const watermarkMs = new Date(ctx.WatermarkValue).getTime();
|
|
2256
|
+
filteredRecords = records.filter(r => {
|
|
2257
|
+
// Check common date fields
|
|
2258
|
+
for (const key of ['updatedAt', 'updated', 'createdAt', 'hs_lastmodifieddate']) {
|
|
2259
|
+
const val = r.Fields[key];
|
|
2260
|
+
if (val && typeof val === 'string') {
|
|
2261
|
+
const recMs = new Date(val).getTime();
|
|
2262
|
+
if (!isNaN(recMs) && recMs > watermarkMs)
|
|
2263
|
+
return true;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
return false;
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
// Pagination cursor — varies by endpoint type
|
|
2270
|
+
let nextCursor;
|
|
2271
|
+
if (isScimPagination) {
|
|
2272
|
+
// SCIM: totalResults lets us know if there are more pages.
|
|
2273
|
+
// Next startIndex = current startIndex + count returned.
|
|
2274
|
+
const totalResults = body['totalResults'] ?? 0;
|
|
2275
|
+
const currentStart = ctx.CurrentCursor ? parseInt(ctx.CurrentCursor, 10) : 1;
|
|
2276
|
+
const nextStart = currentStart + results.length;
|
|
2277
|
+
nextCursor = results.length > 0 && nextStart <= totalResults ? String(nextStart) : undefined;
|
|
2278
|
+
}
|
|
2279
|
+
else {
|
|
2280
|
+
// Standard HubSpot cursor pagination — only trust when results were returned.
|
|
2281
|
+
// Some endpoints (e.g. conversation_threads) return a next.after cursor even on
|
|
2282
|
+
// the last page, causing an infinite loop if followed blindly.
|
|
2283
|
+
const paging = body['paging'];
|
|
2284
|
+
nextCursor = results.length > 0 ? paging?.next?.after : undefined;
|
|
2285
|
+
}
|
|
2286
|
+
// Set watermark from latest record
|
|
2287
|
+
let newWatermark;
|
|
2288
|
+
if (!nextCursor && filteredRecords.length > 0) {
|
|
2289
|
+
newWatermark = this.FindLatestDateInFields(filteredRecords);
|
|
2290
|
+
}
|
|
2291
|
+
return {
|
|
2292
|
+
Records: filteredRecords,
|
|
2293
|
+
HasMore: !!nextCursor,
|
|
2294
|
+
NextCursor: nextCursor,
|
|
2295
|
+
NewWatermarkValue: newWatermark,
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Find the latest date value across common date fields in a set of records.
|
|
2300
|
+
*/
|
|
2301
|
+
FindLatestDateInFields(records) {
|
|
2302
|
+
let latest = 0;
|
|
2303
|
+
let latestStr;
|
|
2304
|
+
const dateKeys = ['updatedAt', 'updated', 'createdAt', 'hs_lastmodifieddate', 'created', 'publishDate'];
|
|
2305
|
+
for (const r of records) {
|
|
2306
|
+
for (const key of dateKeys) {
|
|
2307
|
+
const val = r.Fields[key];
|
|
2308
|
+
if (val && typeof val === 'string') {
|
|
2309
|
+
const ms = new Date(val).getTime();
|
|
2310
|
+
if (!isNaN(ms) && ms > latest) {
|
|
2311
|
+
latest = ms;
|
|
2312
|
+
latestStr = val;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
return latestStr;
|
|
2318
|
+
}
|
|
2319
|
+
/**
|
|
2320
|
+
* Fetches changed records using the HubSpot search API with server-side date filtering.
|
|
2321
|
+
* Much more efficient than fetching ALL records and filtering client-side.
|
|
2322
|
+
*/
|
|
2323
|
+
async FetchChangesViaSearch(ctx) {
|
|
2324
|
+
const companyIntegration = ctx.CompanyIntegration;
|
|
2325
|
+
const contextUser = ctx.ContextUser;
|
|
2326
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
2327
|
+
const headers = this.BuildHeaders(auth);
|
|
2328
|
+
const dateField = this.GetWatermarkField(ctx.ObjectName);
|
|
2329
|
+
const watermarkMs = new Date(ctx.WatermarkValue).getTime();
|
|
2330
|
+
const properties = this.BuildEffectiveProperties(ctx.ObjectName, ctx.RequestedSourceFields);
|
|
2331
|
+
const pageSize = Math.min(ctx.BatchSize ?? 100, 100); // HubSpot search API max is 100
|
|
2332
|
+
const searchBody = {
|
|
2333
|
+
filterGroups: [{
|
|
2334
|
+
filters: [{
|
|
2335
|
+
propertyName: dateField,
|
|
2336
|
+
operator: 'GTE',
|
|
2337
|
+
value: String(watermarkMs),
|
|
2338
|
+
}],
|
|
2339
|
+
}],
|
|
2340
|
+
sorts: [{ propertyName: dateField, direction: 'ASCENDING' }],
|
|
2341
|
+
properties,
|
|
2342
|
+
limit: pageSize,
|
|
2343
|
+
};
|
|
2344
|
+
if (ctx.CurrentCursor) {
|
|
2345
|
+
searchBody['after'] = ctx.CurrentCursor;
|
|
2346
|
+
}
|
|
2347
|
+
const url = `${HUBSPOT_API_BASE}/crm/v3/objects/${ctx.ObjectName}/search`;
|
|
2348
|
+
const response = await this.MakeHTTPRequest(auth, url, 'POST', headers, searchBody);
|
|
2349
|
+
this.ValidateCRUDResponse(response, 'FetchChangesViaSearch', ctx.ObjectName);
|
|
2350
|
+
const body = response.Body;
|
|
2351
|
+
// HubSpot Search API hard limit: 10,000 results maximum per query window.
|
|
2352
|
+
// If total > 10K, only the oldest-modified records (sorted ASCENDING) are returned.
|
|
2353
|
+
// The watermark advances to the batch's max date each cycle, so subsequent syncs
|
|
2354
|
+
// pick up the remainder — no records are permanently lost, but multiple sync cycles
|
|
2355
|
+
// are needed to catch up. Log a warning so operators can monitor.
|
|
2356
|
+
const searchTotal = body.total ?? 0;
|
|
2357
|
+
if (searchTotal > 10_000) {
|
|
2358
|
+
console.warn(`[HubSpot] ${ctx.ObjectName}: Search API returned total=${searchTotal} but is capped at 10,000 results. ` +
|
|
2359
|
+
`Sync will require ${Math.ceil(searchTotal / 10_000)} cycles to catch up from watermark ${ctx.WatermarkValue}. ` +
|
|
2360
|
+
`Consider reducing the sync interval or enabling continuous sync for this object.`);
|
|
2361
|
+
}
|
|
2362
|
+
const rawResults = body.results ?? [];
|
|
2363
|
+
const records = rawResults.map(r => {
|
|
2364
|
+
const raw = r;
|
|
2365
|
+
const flat = this.FlattenHubSpotRecord(raw);
|
|
2366
|
+
return {
|
|
2367
|
+
ExternalID: String(flat['hs_object_id'] ?? raw['id'] ?? ''),
|
|
2368
|
+
ObjectType: ctx.ObjectName,
|
|
2369
|
+
Fields: flat,
|
|
2370
|
+
};
|
|
2371
|
+
});
|
|
2372
|
+
const nextCursor = body.paging?.next?.after;
|
|
2373
|
+
const hasMore = nextCursor != null;
|
|
2374
|
+
// On the final page of active records, also fetch archived (deleted) records
|
|
2375
|
+
// since the watermark so they flow through the engine's delete pipeline.
|
|
2376
|
+
if (!hasMore) {
|
|
2377
|
+
const archived = await this.FetchArchivedCRMChanges(auth, headers, ctx.ObjectName, dateField, watermarkMs, properties);
|
|
2378
|
+
if (archived.length > 0) {
|
|
2379
|
+
console.log(`[HubSpot] ${ctx.ObjectName}: found ${archived.length} archived (deleted) record(s) since watermark`);
|
|
2380
|
+
records.push(...archived);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
let newWatermark;
|
|
2384
|
+
if (!hasMore) {
|
|
2385
|
+
const latest = this.FindLatestDate(records.filter(r => !r.IsDeleted), dateField);
|
|
2386
|
+
if (latest)
|
|
2387
|
+
newWatermark = latest;
|
|
2388
|
+
}
|
|
2389
|
+
return {
|
|
2390
|
+
Records: records,
|
|
2391
|
+
HasMore: hasMore,
|
|
2392
|
+
NextCursor: nextCursor,
|
|
2393
|
+
NewWatermarkValue: newWatermark,
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
/**
|
|
2397
|
+
* Detects archived (deleted) CRM records since the given watermark.
|
|
2398
|
+
*
|
|
2399
|
+
* Uses the HubSpot search API with `archived: true` so the same server-side
|
|
2400
|
+
* GTE watermark filter applies — only records archived/modified since the last
|
|
2401
|
+
* sync are returned. Returns them with `IsDeleted: true` so the integration
|
|
2402
|
+
* engine routes them through the delete pipeline.
|
|
2403
|
+
*
|
|
2404
|
+
* Falls back to empty on any API error (e.g. object type doesn't support
|
|
2405
|
+
* archived search) so delete detection degrades gracefully rather than
|
|
2406
|
+
* blocking the active-record sync.
|
|
2407
|
+
*/
|
|
2408
|
+
async FetchArchivedCRMChanges(auth, headers, objectName, dateField, watermarkMs, properties) {
|
|
2409
|
+
const url = `${HUBSPOT_API_BASE}/crm/v3/objects/${objectName}/search`;
|
|
2410
|
+
const allDeleted = [];
|
|
2411
|
+
let cursor;
|
|
2412
|
+
do {
|
|
2413
|
+
const searchBody = {
|
|
2414
|
+
filterGroups: [{
|
|
2415
|
+
filters: [{
|
|
2416
|
+
propertyName: dateField,
|
|
2417
|
+
operator: 'GTE',
|
|
2418
|
+
value: String(watermarkMs),
|
|
2419
|
+
}],
|
|
2420
|
+
}],
|
|
2421
|
+
sorts: [{ propertyName: dateField, direction: 'ASCENDING' }],
|
|
2422
|
+
properties,
|
|
2423
|
+
limit: 100,
|
|
2424
|
+
archived: true,
|
|
2425
|
+
};
|
|
2426
|
+
if (cursor)
|
|
2427
|
+
searchBody['after'] = cursor;
|
|
2428
|
+
const response = await this.MakeHTTPRequest(auth, url, 'POST', headers, searchBody);
|
|
2429
|
+
if (response.Status !== 200)
|
|
2430
|
+
break; // silently skip — object may not support archived search
|
|
2431
|
+
const body = response.Body;
|
|
2432
|
+
const rawResults = body.results ?? [];
|
|
2433
|
+
for (const r of rawResults) {
|
|
2434
|
+
const raw = r;
|
|
2435
|
+
const flat = this.FlattenHubSpotRecord(raw);
|
|
2436
|
+
allDeleted.push({
|
|
2437
|
+
ExternalID: String(flat['hs_object_id'] ?? raw['id'] ?? ''),
|
|
2438
|
+
ObjectType: objectName,
|
|
2439
|
+
Fields: flat,
|
|
2440
|
+
IsDeleted: true,
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
cursor = rawResults.length > 0 ? body.paging?.next?.after : undefined;
|
|
2444
|
+
} while (cursor);
|
|
2445
|
+
return allDeleted;
|
|
2446
|
+
}
|
|
2447
|
+
/** Returns the watermark date field name for a given object type. */
|
|
2448
|
+
GetWatermarkField(objectName) {
|
|
2449
|
+
return objectName.toLowerCase() === 'contacts' ? 'lastmodifieddate' : 'hs_lastmodifieddate';
|
|
865
2450
|
}
|
|
866
2451
|
/** Finds the latest date value across records for a given field name. */
|
|
867
2452
|
FindLatestDate(records, fieldName) {
|
|
@@ -884,6 +2469,11 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
|
|
|
884
2469
|
* Uses ctx.CurrentOffset to track parent position across batch calls, so the engine
|
|
885
2470
|
* can page through all parents without truncating records.
|
|
886
2471
|
*/
|
|
2472
|
+
/**
|
|
2473
|
+
* Fetches association records using the HubSpot v4 batch/read endpoint.
|
|
2474
|
+
* Batches up to 100 parent IDs per request instead of one GET per parent,
|
|
2475
|
+
* reducing API calls from O(n) to O(n/100).
|
|
2476
|
+
*/
|
|
887
2477
|
async FetchAssociationChanges(ctx, obj) {
|
|
888
2478
|
const parsed = this.ParseAssociationPath(obj.APIPath);
|
|
889
2479
|
if (!parsed) {
|
|
@@ -891,74 +2481,57 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
|
|
|
891
2481
|
return { Records: [], HasMore: false };
|
|
892
2482
|
}
|
|
893
2483
|
const { fromType, toType } = parsed;
|
|
894
|
-
const fields = this.GetCachedFields(obj.ID);
|
|
895
2484
|
const auth = await this.Authenticate(ctx.CompanyIntegration, ctx.ContextUser);
|
|
896
|
-
|
|
897
|
-
//
|
|
898
|
-
const
|
|
899
|
-
const leftFieldName =
|
|
900
|
-
const rightFieldName =
|
|
2485
|
+
// Derive PK field names from the API path object type names.
|
|
2486
|
+
// "companies" → "company_id", "emails" → "email_id", "contacts" → "contact_id"
|
|
2487
|
+
const singularize = (t) => t.endsWith('ies') ? t.slice(0, -3) + 'y' : t.endsWith('s') ? t.slice(0, -1) : t;
|
|
2488
|
+
const leftFieldName = `${singularize(fromType)}_id`;
|
|
2489
|
+
const rightFieldName = `${singularize(toType)}_id`;
|
|
901
2490
|
const parentIDs = await this.LoadAssociationParentIDs(fromType, ctx);
|
|
902
2491
|
const parentOffset = ctx.CurrentOffset ?? 0;
|
|
2492
|
+
const BATCH_LIMIT = 100; // HubSpot v4 batch/read max inputs per request
|
|
903
2493
|
if (parentOffset === 0) {
|
|
904
|
-
console.log(`[HubSpot] Fetching ${obj.Name}: ${parentIDs.length} parent ${fromType}
|
|
905
|
-
}
|
|
906
|
-
const batchSize = Math.min(ctx.BatchSize ?? 200, 200);
|
|
907
|
-
const batchRecords = [];
|
|
908
|
-
let parentIndex = parentOffset;
|
|
909
|
-
while (parentIndex < parentIDs.length && batchRecords.length < batchSize) {
|
|
910
|
-
const parentID = parentIDs[parentIndex];
|
|
911
|
-
const basePath = `${baseURL}/crm/v4/objects/${fromType}/${parentID}/associations/${toType}`;
|
|
912
|
-
const rawRecords = await this.FetchAllAssociationPages(auth, basePath, obj.DefaultPageSize ?? 500);
|
|
913
|
-
let addedAll = true;
|
|
914
|
-
for (const record of rawRecords) {
|
|
915
|
-
if (batchRecords.length >= batchSize) {
|
|
916
|
-
addedAll = false;
|
|
917
|
-
break;
|
|
918
|
-
}
|
|
919
|
-
const flat = this.FlattenAssociationRecord(record, leftFieldName, parentID, rightFieldName);
|
|
920
|
-
batchRecords.push({
|
|
921
|
-
ExternalID: `${flat[leftFieldName]}|${flat[rightFieldName]}`,
|
|
922
|
-
ObjectType: ctx.ObjectName,
|
|
923
|
-
Fields: flat,
|
|
924
|
-
});
|
|
925
|
-
}
|
|
926
|
-
if (addedAll) {
|
|
927
|
-
parentIndex++;
|
|
928
|
-
}
|
|
929
|
-
else {
|
|
930
|
-
// Don't advance — next batch re-fetches this parent so no records are lost
|
|
931
|
-
break;
|
|
932
|
-
}
|
|
2494
|
+
console.log(`[HubSpot] Fetching ${obj.Name}: ${parentIDs.length} parent ${fromType} via batch API (100/request)`);
|
|
933
2495
|
}
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
|
|
2496
|
+
const batchParentIDs = parentIDs.slice(parentOffset, parentOffset + BATCH_LIMIT);
|
|
2497
|
+
if (batchParentIDs.length === 0) {
|
|
2498
|
+
return { Records: [], HasMore: false };
|
|
2499
|
+
}
|
|
2500
|
+
const records = await this.FetchAssociationBatch(auth, fromType, toType, batchParentIDs, leftFieldName, rightFieldName, ctx.ObjectName);
|
|
2501
|
+
const nextOffset = parentOffset + batchParentIDs.length;
|
|
2502
|
+
const hasMore = nextOffset < parentIDs.length;
|
|
2503
|
+
console.log(`[HubSpot] ${obj.Name}: fetched ${records.length} association records (parents ${parentOffset}–${parentOffset + batchParentIDs.length - 1} of ${parentIDs.length})`);
|
|
2504
|
+
return { Records: records, HasMore: hasMore, NextOffset: hasMore ? nextOffset : undefined };
|
|
937
2505
|
}
|
|
938
2506
|
/**
|
|
939
|
-
*
|
|
940
|
-
*
|
|
2507
|
+
* Calls POST /crm/v4/associations/{fromType}/{toType}/batch/read with up to 100 parent IDs.
|
|
2508
|
+
* Response format: { results: [{ from: { id }, to: [{ toObjectId, associationTypes }] }] }
|
|
941
2509
|
*/
|
|
942
|
-
async
|
|
2510
|
+
async FetchAssociationBatch(auth, fromType, toType, parentIDs, leftFieldName, rightFieldName, objectName) {
|
|
943
2511
|
const headers = this.BuildHeaders(auth);
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
2512
|
+
const url = `${HUBSPOT_API_BASE}/crm/v4/associations/${fromType}/${toType}/batch/read`;
|
|
2513
|
+
const body = { inputs: parentIDs.map(id => ({ id })) };
|
|
2514
|
+
const response = await this.MakeHTTPRequest(auth, url, 'POST', headers, body);
|
|
2515
|
+
if (response.Status < 200 || response.Status >= 300) {
|
|
2516
|
+
const respBody = response.Body;
|
|
2517
|
+
const msg = respBody?.message ?? respBody?.error ?? JSON.stringify(respBody);
|
|
2518
|
+
console.warn(`[HubSpot] Association batch read failed for ${objectName}: HTTP ${response.Status} — ${msg}`);
|
|
2519
|
+
return [];
|
|
2520
|
+
}
|
|
2521
|
+
const respBody = response.Body;
|
|
2522
|
+
const records = [];
|
|
2523
|
+
for (const item of respBody.results ?? []) {
|
|
2524
|
+
const parentID = item.from.id;
|
|
2525
|
+
for (const assoc of item.to ?? []) {
|
|
2526
|
+
const flat = this.FlattenAssociationRecord({ toObjectId: assoc.toObjectId, associationTypes: assoc.associationTypes }, leftFieldName, parentID, rightFieldName);
|
|
2527
|
+
records.push({
|
|
2528
|
+
ExternalID: `${flat[leftFieldName]}|${flat[rightFieldName]}`,
|
|
2529
|
+
ObjectType: objectName,
|
|
2530
|
+
Fields: flat,
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
return records;
|
|
962
2535
|
}
|
|
963
2536
|
/**
|
|
964
2537
|
* Converts a HubSpot v4 association result item into a flat record suitable for storage.
|
|
@@ -966,9 +2539,10 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
|
|
|
966
2539
|
*/
|
|
967
2540
|
FlattenAssociationRecord(record, leftFieldName, leftValue, rightFieldName) {
|
|
968
2541
|
const assocTypes = record['associationTypes'];
|
|
2542
|
+
const rightValue = String(record['toObjectId']);
|
|
969
2543
|
return {
|
|
970
2544
|
[leftFieldName]: leftValue,
|
|
971
|
-
[rightFieldName]:
|
|
2545
|
+
[rightFieldName]: rightValue,
|
|
972
2546
|
association_type: assocTypes?.[0]?.label ?? null,
|
|
973
2547
|
};
|
|
974
2548
|
}
|
|
@@ -1031,12 +2605,37 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
|
|
|
1031
2605
|
const obj = HUBSPOT_OBJECTS.find(o => o.Name === objectName);
|
|
1032
2606
|
return obj ? obj.Fields.map(f => f.Name) : [];
|
|
1033
2607
|
}
|
|
2608
|
+
/**
|
|
2609
|
+
* Returns the effective property list for a HubSpot CRM request.
|
|
2610
|
+
*
|
|
2611
|
+
* When `requestedFields` (from FetchContext.RequestedSourceFields) is provided it
|
|
2612
|
+
* contains the source fields from active field maps, including any custom properties.
|
|
2613
|
+
* We merge those with the essential system properties so watermark tracking always works.
|
|
2614
|
+
*
|
|
2615
|
+
* Falls back to the static HUBSPOT_OBJECTS field list when no requestedFields are given.
|
|
2616
|
+
*
|
|
2617
|
+
* Note: hs_object_id is NOT included — it's the top-level `id` field on every HubSpot
|
|
2618
|
+
* response and is injected by FlattenHubSpotRecord regardless of `?properties=`.
|
|
2619
|
+
* Essential system properties (always included):
|
|
2620
|
+
* - GetWatermarkField(objectName) — the per-object modified-date property (object-specific;
|
|
2621
|
+
* contacts use 'lastmodifieddate', all others use 'hs_lastmodifieddate')
|
|
2622
|
+
* - createdate — creation timestamp
|
|
2623
|
+
*/
|
|
2624
|
+
BuildEffectiveProperties(objectName, requestedFields) {
|
|
2625
|
+
const essentialProperties = [this.GetWatermarkField(objectName), 'createdate'];
|
|
2626
|
+
if (requestedFields && requestedFields.length > 0) {
|
|
2627
|
+
const merged = new Set([...requestedFields, ...essentialProperties]);
|
|
2628
|
+
return [...merged];
|
|
2629
|
+
}
|
|
2630
|
+
return this.GetObjectFieldNames(objectName);
|
|
2631
|
+
}
|
|
1034
2632
|
/**
|
|
1035
2633
|
* Builds the `properties` query parameter for a HubSpot object type.
|
|
2634
|
+
* Accepts optional `requestedFields` from FetchContext to include custom-mapped properties.
|
|
1036
2635
|
* Returns empty string if no properties are configured for the object.
|
|
1037
2636
|
*/
|
|
1038
|
-
BuildPropertiesParam(objectName) {
|
|
1039
|
-
const properties = this.
|
|
2637
|
+
BuildPropertiesParam(objectName, requestedFields) {
|
|
2638
|
+
const properties = this.BuildEffectiveProperties(objectName, requestedFields);
|
|
1040
2639
|
if (properties.length > 0) {
|
|
1041
2640
|
return `&properties=${properties.join(',')}`;
|
|
1042
2641
|
}
|
|
@@ -1082,7 +2681,7 @@ let HubSpotConnector = class HubSpotConnector extends BaseRESTIntegrationConnect
|
|
|
1082
2681
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1083
2682
|
}
|
|
1084
2683
|
};
|
|
1085
|
-
HubSpotConnector = __decorate([
|
|
2684
|
+
HubSpotConnector = HubSpotConnector_1 = __decorate([
|
|
1086
2685
|
RegisterClass(BaseIntegrationConnector, 'HubSpotConnector')
|
|
1087
2686
|
], HubSpotConnector);
|
|
1088
2687
|
export { HubSpotConnector };
|