@memberjunction/integration-connectors 5.27.0 → 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.
@@ -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
- // ── Per-instance config accessors (fall back to module-level defaults) ──
331
- get effectiveMaxRetries() { return this._config?.MaxRetries ?? MAX_RETRIES; }
332
- get effectiveRequestTimeoutMs() { return this._config?.RequestTimeoutMs ?? REQUEST_TIMEOUT_MS; }
333
- get effectiveMinRequestIntervalMs() { return this._config?.MinRequestIntervalMs ?? MIN_REQUEST_INTERVAL_MS; }
334
- // ─── Capability Getters ──────────────────────────────────────────────
335
- get SupportsCreate() { return true; }
336
- get SupportsUpdate() { return true; }
337
- get SupportsDelete() { return true; }
338
- get SupportsSearch() { return true; }
339
- get SupportsListing() { return true; }
340
- get IntegrationName() { return 'HubSpot'; }
341
- // ─── Action Metadata ─────────────────────────────────────────────────
342
- GetIntegrationObjects() {
343
- return HUBSPOT_OBJECTS;
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
- GetActionGeneratorConfig() {
346
- const config = super.GetActionGeneratorConfig();
347
- if (!config)
348
- return null;
349
- config.IconClass = 'fa-brands fa-hubspot';
350
- config.CreateCategory = false; // HubSpot category already exists in metadata/action-categories
351
- return config;
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
- const message = bodyObj?.['message']
515
- ? String(bodyObj['message'])
516
- : `[HubSpot] ${operation} on ${objectName} failed (HTTP ${response.Status})`;
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
- const responseBody = await response.json();
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.Get('Configuration');
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.Get('CredentialID');
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.Get('Configuration');
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 intercept association objects (Category === 'Association')
832
- * and route them through the v4 per-object associations endpoint instead of the
833
- * non-existent flat list endpoint stored in APIPath.
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
- const result = await super.FetchChanges(ctx);
841
- // Contacts use 'lastmodifieddate', all other objects use 'hs_lastmodifieddate'.
842
- const dateField = ctx.ObjectName.toLowerCase() === 'contacts'
843
- ? 'lastmodifieddate' : 'hs_lastmodifieddate';
844
- // Compute new watermark from ALL fetched records BEFORE filtering.
845
- // We need the latest date across the full dataset so the watermark advances correctly.
846
- const allRecords = result.Records;
847
- if (!result.HasMore) {
848
- const latest = this.FindLatestDate(allRecords, dateField);
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
- result.NewWatermarkValue = latest;
851
- }
852
- // Client-side watermark filtering — HubSpot's list API has no server-side date filter.
853
- // We fetch all records but only return ones modified after the watermark.
854
- if (ctx.WatermarkValue && allRecords.length > 0) {
855
- const watermarkDate = new Date(ctx.WatermarkValue);
856
- result.Records = allRecords.filter(r => {
857
- const val = r.Fields[dateField];
858
- if (val == null)
859
- return true;
860
- const d = new Date(String(val));
861
- return isNaN(d.getTime()) || d > watermarkDate;
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
- return result;
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
- const baseURL = this.GetBaseURL(ctx.CompanyIntegration, auth);
897
- // Association objects have two PK fields: left (from-side) and right (to-side)
898
- const pkFields = fields.filter(f => f.IsPrimaryKey).sort((a, b) => a.Sequence - b.Sequence);
899
- const leftFieldName = pkFields[0]?.Name ?? `${fromType.replace(/s$/, '')}_id`;
900
- const rightFieldName = pkFields[1]?.Name ?? `${toType.replace(/s$/, '')}_id`;
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} to iterate`);
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 hasMore = parentIndex < parentIDs.length;
935
- console.log(`[HubSpot] ${obj.Name}: fetched ${batchRecords.length} association records (parents ${parentOffset}-${parentIndex - 1} of ${parentIDs.length})`);
936
- return { Records: batchRecords, HasMore: hasMore, NextOffset: hasMore ? parentIndex : undefined };
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
- * Paginates through all pages of a HubSpot v4 per-object association endpoint,
940
- * returning the raw result items (each with toObjectId + associationTypes).
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 FetchAllAssociationPages(auth, basePath, pageSize) {
2510
+ async FetchAssociationBatch(auth, fromType, toType, parentIDs, leftFieldName, rightFieldName, objectName) {
943
2511
  const headers = this.BuildHeaders(auth);
944
- const allRecords = [];
945
- let cursor;
946
- do {
947
- const url = cursor
948
- ? `${basePath}?limit=${pageSize}&after=${encodeURIComponent(cursor)}`
949
- : `${basePath}?limit=${pageSize}`;
950
- const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
951
- if (response.Status < 200 || response.Status >= 300)
952
- break;
953
- const body = response.Body;
954
- const results = body['results'];
955
- if (!results || !Array.isArray(results) || results.length === 0)
956
- break;
957
- allRecords.push(...results);
958
- const paging = body['paging'];
959
- cursor = paging?.next?.after;
960
- } while (cursor);
961
- return allRecords;
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]: String(record['toObjectId']),
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.GetObjectFieldNames(objectName);
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 };