@memberjunction/integration-connectors 5.51.0 → 6.1.0-edge.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.
Files changed (155) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +109 -277
  3. package/dist/index.d.ts +0 -36
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +7 -38
  6. package/dist/index.js.map +1 -1
  7. package/package.json +14 -19
  8. package/dist/AptifyConnector.d.ts +0 -95
  9. package/dist/AptifyConnector.d.ts.map +0 -1
  10. package/dist/AptifyConnector.js +0 -760
  11. package/dist/AptifyConnector.js.map +0 -1
  12. package/dist/BlackbaudConnector.d.ts +0 -64
  13. package/dist/BlackbaudConnector.d.ts.map +0 -1
  14. package/dist/BlackbaudConnector.js +0 -916
  15. package/dist/BlackbaudConnector.js.map +0 -1
  16. package/dist/ConstantContactConnector.d.ts +0 -137
  17. package/dist/ConstantContactConnector.d.ts.map +0 -1
  18. package/dist/ConstantContactConnector.js +0 -663
  19. package/dist/ConstantContactConnector.js.map +0 -1
  20. package/dist/CventConnector.d.ts +0 -146
  21. package/dist/CventConnector.d.ts.map +0 -1
  22. package/dist/CventConnector.js +0 -540
  23. package/dist/CventConnector.js.map +0 -1
  24. package/dist/DynamicsDataverseConnector.d.ts +0 -296
  25. package/dist/DynamicsDataverseConnector.d.ts.map +0 -1
  26. package/dist/DynamicsDataverseConnector.js +0 -790
  27. package/dist/DynamicsDataverseConnector.js.map +0 -1
  28. package/dist/FileFeedConnector.d.ts +0 -64
  29. package/dist/FileFeedConnector.d.ts.map +0 -1
  30. package/dist/FileFeedConnector.js +0 -190
  31. package/dist/FileFeedConnector.js.map +0 -1
  32. package/dist/FontevaConnector.d.ts +0 -153
  33. package/dist/FontevaConnector.d.ts.map +0 -1
  34. package/dist/FontevaConnector.js +0 -421
  35. package/dist/FontevaConnector.js.map +0 -1
  36. package/dist/GrowthZoneConnector.d.ts +0 -183
  37. package/dist/GrowthZoneConnector.d.ts.map +0 -1
  38. package/dist/GrowthZoneConnector.js +0 -702
  39. package/dist/GrowthZoneConnector.js.map +0 -1
  40. package/dist/HivebriteConnector.d.ts +0 -151
  41. package/dist/HivebriteConnector.d.ts.map +0 -1
  42. package/dist/HivebriteConnector.js +0 -533
  43. package/dist/HivebriteConnector.js.map +0 -1
  44. package/dist/HubSpotConnector.d.ts +0 -523
  45. package/dist/HubSpotConnector.d.ts.map +0 -1
  46. package/dist/HubSpotConnector.js +0 -3110
  47. package/dist/HubSpotConnector.js.map +0 -1
  48. package/dist/IMISConnector.d.ts +0 -171
  49. package/dist/IMISConnector.d.ts.map +0 -1
  50. package/dist/IMISConnector.js +0 -695
  51. package/dist/IMISConnector.js.map +0 -1
  52. package/dist/MJToMJConnector.d.ts +0 -279
  53. package/dist/MJToMJConnector.d.ts.map +0 -1
  54. package/dist/MJToMJConnector.js +0 -893
  55. package/dist/MJToMJConnector.js.map +0 -1
  56. package/dist/MagnetMailConnector.d.ts +0 -163
  57. package/dist/MagnetMailConnector.d.ts.map +0 -1
  58. package/dist/MagnetMailConnector.js +0 -823
  59. package/dist/MagnetMailConnector.js.map +0 -1
  60. package/dist/MailchimpConnector.d.ts +0 -197
  61. package/dist/MailchimpConnector.d.ts.map +0 -1
  62. package/dist/MailchimpConnector.js +0 -791
  63. package/dist/MailchimpConnector.js.map +0 -1
  64. package/dist/MemberSuiteConnector.d.ts +0 -185
  65. package/dist/MemberSuiteConnector.d.ts.map +0 -1
  66. package/dist/MemberSuiteConnector.js +0 -762
  67. package/dist/MemberSuiteConnector.js.map +0 -1
  68. package/dist/NeonCRMConnector.d.ts +0 -273
  69. package/dist/NeonCRMConnector.d.ts.map +0 -1
  70. package/dist/NeonCRMConnector.js +0 -1039
  71. package/dist/NeonCRMConnector.js.map +0 -1
  72. package/dist/NetForumConnector.d.ts +0 -170
  73. package/dist/NetForumConnector.d.ts.map +0 -1
  74. package/dist/NetForumConnector.js +0 -778
  75. package/dist/NetForumConnector.js.map +0 -1
  76. package/dist/NetSuiteConnector.d.ts +0 -136
  77. package/dist/NetSuiteConnector.d.ts.map +0 -1
  78. package/dist/NetSuiteConnector.js +0 -703
  79. package/dist/NetSuiteConnector.js.map +0 -1
  80. package/dist/NimbleAMSConnector.d.ts +0 -142
  81. package/dist/NimbleAMSConnector.d.ts.map +0 -1
  82. package/dist/NimbleAMSConnector.js +0 -703
  83. package/dist/NimbleAMSConnector.js.map +0 -1
  84. package/dist/NoviConnector.d.ts +0 -178
  85. package/dist/NoviConnector.d.ts.map +0 -1
  86. package/dist/NoviConnector.js +0 -566
  87. package/dist/NoviConnector.js.map +0 -1
  88. package/dist/ORCIDConnector.d.ts +0 -207
  89. package/dist/ORCIDConnector.d.ts.map +0 -1
  90. package/dist/ORCIDConnector.js +0 -708
  91. package/dist/ORCIDConnector.js.map +0 -1
  92. package/dist/OpenWaterConnector.d.ts +0 -173
  93. package/dist/OpenWaterConnector.d.ts.map +0 -1
  94. package/dist/OpenWaterConnector.js +0 -893
  95. package/dist/OpenWaterConnector.js.map +0 -1
  96. package/dist/PathLMSConnector.d.ts +0 -395
  97. package/dist/PathLMSConnector.d.ts.map +0 -1
  98. package/dist/PathLMSConnector.js +0 -1281
  99. package/dist/PathLMSConnector.js.map +0 -1
  100. package/dist/PheedLoopConnector.d.ts +0 -119
  101. package/dist/PheedLoopConnector.d.ts.map +0 -1
  102. package/dist/PheedLoopConnector.js +0 -429
  103. package/dist/PheedLoopConnector.js.map +0 -1
  104. package/dist/PropFuelConnector.d.ts +0 -153
  105. package/dist/PropFuelConnector.d.ts.map +0 -1
  106. package/dist/PropFuelConnector.js +0 -450
  107. package/dist/PropFuelConnector.js.map +0 -1
  108. package/dist/QuickBooksConnector.d.ts +0 -92
  109. package/dist/QuickBooksConnector.d.ts.map +0 -1
  110. package/dist/QuickBooksConnector.js +0 -713
  111. package/dist/QuickBooksConnector.js.map +0 -1
  112. package/dist/RasaConnector.d.ts +0 -152
  113. package/dist/RasaConnector.d.ts.map +0 -1
  114. package/dist/RasaConnector.js +0 -929
  115. package/dist/RasaConnector.js.map +0 -1
  116. package/dist/Reach360Connector.d.ts +0 -175
  117. package/dist/Reach360Connector.d.ts.map +0 -1
  118. package/dist/Reach360Connector.js +0 -796
  119. package/dist/Reach360Connector.js.map +0 -1
  120. package/dist/RelationalDBConnector.d.ts +0 -108
  121. package/dist/RelationalDBConnector.d.ts.map +0 -1
  122. package/dist/RelationalDBConnector.js +0 -228
  123. package/dist/RelationalDBConnector.js.map +0 -1
  124. package/dist/RhythmConnector.d.ts +0 -188
  125. package/dist/RhythmConnector.d.ts.map +0 -1
  126. package/dist/RhythmConnector.js +0 -784
  127. package/dist/RhythmConnector.js.map +0 -1
  128. package/dist/SageIntacctConnector.d.ts +0 -337
  129. package/dist/SageIntacctConnector.d.ts.map +0 -1
  130. package/dist/SageIntacctConnector.js +0 -2081
  131. package/dist/SageIntacctConnector.js.map +0 -1
  132. package/dist/SalesforceConnector.d.ts +0 -461
  133. package/dist/SalesforceConnector.d.ts.map +0 -1
  134. package/dist/SalesforceConnector.js +0 -1862
  135. package/dist/SalesforceConnector.js.map +0 -1
  136. package/dist/SharePointConnector.d.ts +0 -188
  137. package/dist/SharePointConnector.d.ts.map +0 -1
  138. package/dist/SharePointConnector.js +0 -716
  139. package/dist/SharePointConnector.js.map +0 -1
  140. package/dist/WicketConnector.d.ts +0 -252
  141. package/dist/WicketConnector.d.ts.map +0 -1
  142. package/dist/WicketConnector.js +0 -938
  143. package/dist/WicketConnector.js.map +0 -1
  144. package/dist/WildApricotConnector.d.ts +0 -201
  145. package/dist/WildApricotConnector.d.ts.map +0 -1
  146. package/dist/WildApricotConnector.js +0 -1129
  147. package/dist/WildApricotConnector.js.map +0 -1
  148. package/dist/YourMembershipConnector.d.ts +0 -349
  149. package/dist/YourMembershipConnector.d.ts.map +0 -1
  150. package/dist/YourMembershipConnector.js +0 -4062
  151. package/dist/YourMembershipConnector.js.map +0 -1
  152. package/dist/generate-integration-actions.d.ts +0 -23
  153. package/dist/generate-integration-actions.d.ts.map +0 -1
  154. package/dist/generate-integration-actions.js +0 -201
  155. package/dist/generate-integration-actions.js.map +0 -1
@@ -1,929 +0,0 @@
1
- var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
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
- return c > 3 && r && Object.defineProperty(target, key, r), r;
6
- };
7
- import { RegisterClass } from '@memberjunction/global';
8
- import { Metadata } from '@memberjunction/core';
9
- import { BaseIntegrationConnector, BaseRESTIntegrationConnector, } from '@memberjunction/integration-engine';
10
- // ─── Constants ───────────────────────────────────────────────────────
11
- /** Rasa.io API base URL */
12
- const RASA_API_BASE = 'https://api.rasa.io/v1';
13
- /** JWT tokens are refreshed every 50 minutes (tokens last ~60 min) */
14
- const TOKEN_TTL_MS = 50 * 60 * 1000;
15
- /** Maximum retries for rate-limited or failed requests */
16
- const MAX_RETRIES = 3;
17
- /** HTTP request timeout in milliseconds */
18
- const REQUEST_TIMEOUT_MS = 60000;
19
- /** Minimum milliseconds between API requests */
20
- const MIN_REQUEST_INTERVAL_MS = 200;
21
- /** Default page size for Rasa.io API calls */
22
- const DEFAULT_PAGE_SIZE = 50;
23
- // ─── Connector Implementation ────────────────────────────────────────
24
- /**
25
- * Connector for the Rasa.io newsletter platform REST API.
26
- *
27
- * Extends BaseRESTIntegrationConnector to leverage metadata-driven object/field
28
- * discovery from IntegrationEngineBase cache and generic pagination handling.
29
- *
30
- * Auth flow:
31
- * 1. POST /v1/tokens with Basic auth header + { "key": "<API-key>" } body
32
- * 2. Receive JWT token in response
33
- * 3. Pass JWT as rasa-token header on all data requests
34
- *
35
- * Pagination:
36
- * - persons/posts: offset-based (skip/limit), supports updated_since watermark
37
- * - insights/actions: cursor-based (base64 skip token from next_link), supports created_since watermark
38
- * - insights/topics: no pagination, full replace on each sync
39
- *
40
- * Response envelope: { code, metadata: { next_link, record_count }, results: [...] }
41
- */
42
- // ─── Action Metadata Objects ──────────────────────────────────────────
43
- const RASA_ACTION_OBJECTS = [
44
- {
45
- Name: 'persons', DisplayName: 'Person',
46
- Description: 'A newsletter subscriber/contact in Rasa.io', SupportsWrite: false,
47
- Fields: [
48
- { Name: 'email', DisplayName: 'Email', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Subscriber email address' },
49
- { Name: 'first_name', DisplayName: 'First Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Subscriber first name' },
50
- { Name: 'last_name', DisplayName: 'Last Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Subscriber last name' },
51
- { Name: 'external_id', DisplayName: 'Source External ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'External ID from source system' },
52
- { Name: 'is_active', DisplayName: 'Is Active', Type: 'boolean', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Whether the subscriber is active' },
53
- { Name: 'is_subscribed', DisplayName: 'Is Subscribed', Type: 'boolean', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Whether the subscriber is subscribed' },
54
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Rasa.io internal person ID' },
55
- { Name: 'created', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When the person was created' },
56
- { Name: 'updated', DisplayName: 'Updated Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last updated' },
57
- ],
58
- },
59
- {
60
- Name: 'posts', DisplayName: 'Post',
61
- Description: 'A newsletter content article/post in Rasa.io', SupportsWrite: false,
62
- Fields: [
63
- { Name: 'title', DisplayName: 'Title', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Post title' },
64
- { Name: 'description', DisplayName: 'Description', Type: 'text', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Post description/summary' },
65
- { Name: 'url', DisplayName: 'URL', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Post URL' },
66
- { Name: 'source_url', DisplayName: 'Source URL', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Original source URL' },
67
- { Name: 'image_url', DisplayName: 'Image URL', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Post image URL' },
68
- { Name: 'quality_score', DisplayName: 'Quality Score', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Content quality score' },
69
- { Name: 'is_active', DisplayName: 'Is Active', Type: 'boolean', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Whether the post is active' },
70
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Rasa.io internal post ID' },
71
- { Name: 'created', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When the post was created' },
72
- { Name: 'updated', DisplayName: 'Updated Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When last updated' },
73
- ],
74
- },
75
- {
76
- Name: 'insights-actions', DisplayName: 'Insight Action',
77
- Description: 'User action/engagement events from Rasa.io analytics', SupportsWrite: false,
78
- Fields: [
79
- { Name: 'person_id', DisplayName: 'Person ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Associated person ID' },
80
- { Name: 'email', DisplayName: 'Email', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Person email' },
81
- { Name: 'action_type', DisplayName: 'Action Type', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Type of action (click, open, etc.)' },
82
- { Name: 'created', DisplayName: 'Created Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'When the action occurred' },
83
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Action event ID' },
84
- ],
85
- },
86
- {
87
- Name: 'insights-topics', DisplayName: 'Insight Topic',
88
- Description: 'User topic interest data from Rasa.io analytics', SupportsWrite: false,
89
- Fields: [
90
- { Name: 'person_id', DisplayName: 'Person ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Associated person ID' },
91
- { Name: 'email', DisplayName: 'Email', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Person email' },
92
- { Name: 'topic', DisplayName: 'Topic', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Topic name' },
93
- { Name: 'weight', DisplayName: 'Weight', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Interest weight/score' },
94
- { Name: 'first_click', DisplayName: 'First Click', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'First interaction with this topic' },
95
- { Name: 'last_click', DisplayName: 'Last Click', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Most recent interaction' },
96
- { Name: 'id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'Topic interest ID' },
97
- ],
98
- },
99
- {
100
- Name: 'person-attributes', DisplayName: 'Person Attribute',
101
- Description: 'Flattened custom attribute key-value pairs per person from GET /v1/persons/{id}/attributes. Custom attributes are free-form — any key the client has populated.',
102
- SupportsWrite: false,
103
- Fields: [
104
- { Name: 'person_id', DisplayName: 'Person ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Rasa.io person identifier (composite key)' },
105
- { Name: 'external_id', DisplayName: 'External ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'External system identifier' },
106
- { Name: 'email', DisplayName: 'Email', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Subscriber email' },
107
- { Name: 'attribute_name', DisplayName: 'Attribute Name', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Custom attribute key (composite key)' },
108
- { Name: 'attribute_value', DisplayName: 'Attribute Value', Type: 'text', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Stringified attribute value' },
109
- ],
110
- },
111
- {
112
- Name: 'analytics-activities', DisplayName: 'Activities Analytics',
113
- Description: 'Email activity metrics (opens, clicks, deliveries, bounces) bucketed by date from POST /v1/analytics/activities. Supports date_range / interval / segment filters.',
114
- SupportsWrite: false,
115
- Fields: [
116
- { Name: 'bucket_start', DisplayName: 'Bucket Start', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Bucket start timestamp (composite key)' },
117
- { Name: 'metric_name', DisplayName: 'Metric Name', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Metric name (opens, clicks, ...) — composite key' },
118
- { Name: 'metric_value', DisplayName: 'Metric Value', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Aggregated metric count' },
119
- { Name: 'segment_code', DisplayName: 'Segment Code', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Segment filter' },
120
- { Name: 'interval', DisplayName: 'Interval', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Bucket interval (day/hour/week)' },
121
- { Name: 'timezone', DisplayName: 'Timezone', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Aggregation timezone' },
122
- ],
123
- },
124
- {
125
- Name: 'analytics-articles', DisplayName: 'Articles Analytics',
126
- Description: 'Article click analytics ranked by popularity from POST /v1/analytics/articles.',
127
- SupportsWrite: false,
128
- Fields: [
129
- { Name: 'article_id', DisplayName: 'Article ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Article identifier' },
130
- { Name: 'title', DisplayName: 'Title', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Article title' },
131
- { Name: 'url', DisplayName: 'URL', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Article URL' },
132
- { Name: 'click_count', DisplayName: 'Click Count', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Total clicks' },
133
- { Name: 'unique_click_count', DisplayName: 'Unique Click Count', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Unique click count' },
134
- { Name: 'date_range', DisplayName: 'Date Range', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Date range filter used' },
135
- { Name: 'segment_code', DisplayName: 'Segment Code', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Segment filter' },
136
- ],
137
- },
138
- {
139
- Name: 'analytics-topics', DisplayName: 'Topics Analytics',
140
- Description: 'Topic engagement ranking with unique user counts from POST /v1/analytics/topics.',
141
- SupportsWrite: false,
142
- Fields: [
143
- { Name: 'topic', DisplayName: 'Topic', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Topic name' },
144
- { Name: 'total_interactions', DisplayName: 'Total Interactions', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Total engagement count' },
145
- { Name: 'unique_users', DisplayName: 'Unique Users', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Unique engaged users' },
146
- { Name: 'date_range', DisplayName: 'Date Range', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Date range filter used' },
147
- { Name: 'segment_code', DisplayName: 'Segment Code', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Segment filter' },
148
- { Name: 'timezone', DisplayName: 'Timezone', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Aggregation timezone' },
149
- ],
150
- },
151
- ];
152
- // ─── Analytics query defaults ─────────────────────────────────────────
153
- /** Default date range label when no watermark is present (covers recent history). */
154
- const ANALYTICS_DEFAULT_DATE_RANGE = 'past_month';
155
- /** Default bucket interval for activities analytics. */
156
- const ANALYTICS_DEFAULT_INTERVAL = 'day';
157
- /** Default suspect-click filter. */
158
- const ANALYTICS_DEFAULT_SUSPECT_CLICK = 'real_clicks';
159
- /** Default analytics timezone. */
160
- const ANALYTICS_DEFAULT_TIMEZONE = 'UTC';
161
- /** Default segment code for analytics. */
162
- const ANALYTICS_DEFAULT_SEGMENT = 'All';
163
- /** Object names that require a POST body for analytics endpoints. */
164
- const ANALYTICS_POST_OBJECTS = new Set([
165
- 'analytics-activities',
166
- 'analytics-articles',
167
- 'analytics-topics',
168
- ]);
169
- let RasaConnector = class RasaConnector extends BaseRESTIntegrationConnector {
170
- constructor() {
171
- super(...arguments);
172
- /** Cached JWT token */
173
- this.cachedToken = null;
174
- /** When the cached token was obtained */
175
- this.tokenObtainedAt = 0;
176
- /** Timestamp of the last API request, used for throttling */
177
- this.lastRequestTime = 0;
178
- /** Running count of records fetched for the current object in this sync run */
179
- this._runningFetchTotal = 0;
180
- /** Tracks all ExternalIDs seen per object in this sync run to detect API wrap-around */
181
- this._seenIDs = new Map();
182
- /** Last fetched URL path, used for per-page logging */
183
- this._lastFetchedPath = '';
184
- /** Watermark value for the current FetchChanges call — used in BuildPaginatedURL */
185
- this._currentWatermark = undefined;
186
- /** Object name for the current FetchChanges call — used in NormalizeResponse and BuildPaginatedURL */
187
- this._currentObjectName = '';
188
- /**
189
- * Buffer for non-paginated objects that return more records than BatchSize in one shot.
190
- * Keyed by object name. Records are spliced out as they're served to the engine.
191
- */
192
- this._batchBuffer = new Map();
193
- /**
194
- * Watermark values pre-computed from the full result set when buffering.
195
- * Stored so the final buffer batch can still advance the watermark correctly.
196
- */
197
- this._batchBufferWatermarks = new Map();
198
- }
199
- get IntegrationName() { return 'Rasa.io'; }
200
- GetIntegrationObjects() {
201
- return RASA_ACTION_OBJECTS;
202
- }
203
- GetActionGeneratorConfig() {
204
- const config = super.GetActionGeneratorConfig();
205
- if (!config)
206
- return null;
207
- config.IconClass = 'fa-solid fa-envelope-open-text';
208
- config.CategoryDescription = 'Rasa.io email newsletter integration for managing subscribers, newsletters, and engagement data';
209
- config.ParentCategoryName = 'Communication';
210
- // Rasa.io is read-only but we still want Search/List actions for querying
211
- config.IncludeSearch = true;
212
- config.IncludeList = true;
213
- return config;
214
- }
215
- // ─── Abstract method implementations ─────────────────────────────
216
- async Authenticate(companyIntegration, contextUser) {
217
- console.log(`[Rasa.io] Authenticating...`);
218
- const config = await this.ParseConfig(companyIntegration, contextUser);
219
- const token = await this.GetToken(config);
220
- // Do NOT log credential-derived info (token length, prefix, etc.) — would leak secret-shape data into MJAPI logs.
221
- console.log(`[Rasa.io] Authenticated successfully`);
222
- const auth = { Token: token, Config: config };
223
- return auth;
224
- }
225
- BuildHeaders(auth) {
226
- return {
227
- 'rasa-token': auth.Token,
228
- 'Accept': 'application/json',
229
- 'Content-Type': 'application/json',
230
- };
231
- }
232
- async MakeHTTPRequest(auth, url, method, headers, body) {
233
- const rasaAuth = auth;
234
- // Throttle requests
235
- const elapsed = Date.now() - this.lastRequestTime;
236
- if (elapsed < MIN_REQUEST_INTERVAL_MS) {
237
- await this.Sleep(MIN_REQUEST_INTERVAL_MS - elapsed);
238
- }
239
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
240
- const fetchOptions = {
241
- method,
242
- headers,
243
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
244
- };
245
- if (body && method !== 'GET') {
246
- fetchOptions.body = JSON.stringify(body);
247
- }
248
- let response;
249
- try {
250
- response = await fetch(url, fetchOptions);
251
- }
252
- catch (err) {
253
- if (attempt < MAX_RETRIES && this.IsTimeoutOrNetworkError(err)) {
254
- console.warn(`Rasa.io timeout on ${url}, retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
255
- await this.Sleep(1000 * (attempt + 1));
256
- continue;
257
- }
258
- throw err;
259
- }
260
- if (response.status === 401 && attempt < MAX_RETRIES) {
261
- // Token expired, refresh and retry
262
- console.warn('Rasa.io 401, refreshing token');
263
- this.cachedToken = null;
264
- const newToken = await this.GetToken(rasaAuth.Config);
265
- rasaAuth.Token = newToken;
266
- headers['rasa-token'] = newToken;
267
- continue;
268
- }
269
- if (response.status === 429 && attempt < MAX_RETRIES) {
270
- const delayMs = 2000 * Math.pow(2, attempt);
271
- console.warn(`Rasa.io rate limited (429), retrying in ${delayMs}ms`);
272
- await this.Sleep(delayMs);
273
- continue;
274
- }
275
- this.lastRequestTime = Date.now();
276
- const responseBody = await response.json();
277
- const responseHeaders = {};
278
- response.headers.forEach((value, key) => {
279
- responseHeaders[key.toLowerCase()] = value;
280
- });
281
- return {
282
- Status: response.status,
283
- Body: responseBody,
284
- Headers: responseHeaders,
285
- };
286
- }
287
- throw new Error(`Rasa.io request failed after ${MAX_RETRIES} retries: ${url}`);
288
- }
289
- NormalizeResponse(rawBody, responseDataKey) {
290
- const body = rawBody;
291
- if (body.results && Array.isArray(body.results)) {
292
- const rawResults = body.results;
293
- // insights/topics: flatten nested topics[] per person into individual rows
294
- if (this._currentObjectName.toLowerCase() === 'insights-topics') {
295
- return this.FlattenInsightsTopics(rawResults);
296
- }
297
- // person-attributes: flatten the attributes object per person into one row per (person, attribute)
298
- if (this._currentObjectName.toLowerCase() === 'person-attributes') {
299
- return this.FlattenPersonAttributes(rawResults);
300
- }
301
- // analytics-activities: flatten date-bucketed metric dictionary into rows
302
- if (this._currentObjectName.toLowerCase() === 'analytics-activities') {
303
- return this.FlattenActivitiesAnalytics(rawResults);
304
- }
305
- // analytics-articles / analytics-topics: plain rows — just unwrap data envelopes
306
- // Standard envelope: each item may wrap actual record under 'data' key — unwrap it
307
- const records = rawResults.map(item => {
308
- const inner = item['data'];
309
- return (inner && typeof inner === 'object' && !Array.isArray(inner))
310
- ? inner
311
- : item;
312
- });
313
- this._runningFetchTotal += records.length;
314
- const pathLabel = this._lastFetchedPath || 'unknown';
315
- console.log(`[Rasa.io] Fetched ${records.length} records (running total: ${this._runningFetchTotal}) from ${pathLabel}`);
316
- return records;
317
- }
318
- // If responseDataKey is specified, use it
319
- if (responseDataKey && body[responseDataKey]) {
320
- const data = body[responseDataKey];
321
- return Array.isArray(data) ? data : [data];
322
- }
323
- // Fallback: if body itself is an array
324
- if (Array.isArray(body)) {
325
- return body;
326
- }
327
- return [];
328
- }
329
- ExtractPaginationInfo(rawBody, _paginationType, _currentPage, currentOffset, pageSize) {
330
- const body = rawBody;
331
- const metadata = body.metadata;
332
- if (!metadata) {
333
- return { HasMore: false };
334
- }
335
- const totalRecords = metadata.record_count;
336
- const nextLink = metadata.next_link;
337
- // Count actual records returned in this page
338
- const results = body.results;
339
- const returnedCount = Array.isArray(results) ? results.length : 0;
340
- // If the API returned fewer records than the page size, this is the last page.
341
- // The Rasa API returns next_link even past the end of the dataset, so we cannot
342
- // rely on next_link alone. Fewer records than requested = no more data.
343
- if (returnedCount < pageSize) {
344
- return { HasMore: false, TotalRecords: totalRecords };
345
- }
346
- const hasMore = !!nextLink && nextLink.length > 0;
347
- if (!hasMore) {
348
- return { HasMore: false, TotalRecords: totalRecords };
349
- }
350
- // Determine cursor vs offset by inspecting the skip param in next_link
351
- let nextCursor;
352
- let nextOffset;
353
- try {
354
- const url = new URL(nextLink);
355
- const skipParam = url.searchParams.get('skip');
356
- if (skipParam && isNaN(Number(skipParam))) {
357
- // Non-numeric skip param = base64 cursor token (insights/actions)
358
- nextCursor = skipParam;
359
- }
360
- else {
361
- nextOffset = currentOffset + returnedCount;
362
- }
363
- }
364
- catch {
365
- nextOffset = currentOffset + pageSize;
366
- }
367
- return {
368
- HasMore: true,
369
- NextOffset: nextOffset,
370
- NextCursor: nextCursor,
371
- TotalRecords: totalRecords,
372
- };
373
- }
374
- GetBaseURL(_companyIntegration) {
375
- return RASA_API_BASE;
376
- }
377
- /**
378
- * Builds paginated URL with:
379
- * - Watermark filter params (updated_since / created_since) sent server-side
380
- * - Offset-based skip/limit for persons and posts
381
- * - Cursor-based skip token for insights/actions
382
- * - No pagination params for insights/topics
383
- */
384
- BuildPaginatedURL(basePath, obj, _page, offset, cursor, effectivePageSize) {
385
- try {
386
- this._lastFetchedPath = new URL(basePath).pathname;
387
- }
388
- catch {
389
- this._lastFetchedPath = basePath;
390
- }
391
- const objectName = (obj.Name ?? '').toLowerCase();
392
- const limit = effectivePageSize ?? obj.DefaultPageSize ?? DEFAULT_PAGE_SIZE;
393
- const params = new URLSearchParams();
394
- // Pass watermark filter to API so it only returns changed records
395
- if (this._currentWatermark) {
396
- if (objectName === 'persons' || objectName === 'posts') {
397
- params.set('updated_since', this._currentWatermark);
398
- }
399
- else if (objectName === 'insights-actions') {
400
- params.set('created_since', this._currentWatermark);
401
- }
402
- }
403
- // Pagination params — insights/topics has no pagination
404
- if (objectName !== 'insights-topics') {
405
- if (objectName === 'insights-actions' && cursor) {
406
- // Cursor-based: cursor is the base64 skip token extracted from previous next_link
407
- params.set('skip', cursor);
408
- }
409
- else if (offset > 0) {
410
- // Offset-based
411
- params.set('skip', String(offset));
412
- }
413
- params.set('limit', String(limit));
414
- }
415
- const queryString = params.toString();
416
- return queryString ? `${basePath}?${queryString}` : basePath;
417
- }
418
- // ─── DiscoverObjects ──────────────────────────────────────────────
419
- /**
420
- * Canonical Rasa.io v1 API top-level objects. Rasa.io's schema is
421
- * fixed — there are no per-tenant "custom tables". Custom subscriber
422
- * attributes ARE per-tenant and surface as additional columns on
423
- * `persons` via the existing DiscoverFields path (live-queries
424
- * `/v1/persons/{id}/attributes` and merges into the persons field set).
425
- */
426
- async DiscoverObjects(companyIntegration, contextUser) {
427
- const persisted = await super.DiscoverObjects(companyIntegration, contextUser);
428
- const canonical = [
429
- { Name: 'persons', Label: 'Persons (Subscribers)', Description: 'Subscriber records. v1 API /v1/persons. Per-account custom attributes surface here via DiscoverFields.', SupportsIncrementalSync: true, SupportsWrite: true },
430
- { Name: 'posts', Label: 'Posts (Articles)', Description: 'Newsletter content items / articles. v1 API /v1/posts.', SupportsIncrementalSync: true, SupportsWrite: false },
431
- { Name: 'communities', Label: 'Communities', Description: 'Tenant communities the API key has access to. v1 API /v1/communities.', SupportsIncrementalSync: false, SupportsWrite: false },
432
- { Name: 'analytics.activities', Label: 'Email Activity Analytics', Description: 'Open/click/delivery/bounce metrics bucketed by date. v1 API POST /v1/analytics/activities.', SupportsIncrementalSync: true, SupportsWrite: false },
433
- { Name: 'analytics.articles', Label: 'Article Analytics', Description: 'Article click analytics ranked by popularity. v1 API POST /v1/analytics/articles.', SupportsIncrementalSync: true, SupportsWrite: false },
434
- { Name: 'analytics.topics', Label: 'Topic Engagement Analytics', Description: 'Topic engagement ranking with unique user counts. v1 API POST /v1/analytics/topics.', SupportsIncrementalSync: true, SupportsWrite: false },
435
- ];
436
- const byName = new Map();
437
- for (const o of persisted)
438
- byName.set(o.Name.toLowerCase(), o);
439
- for (const c of canonical) {
440
- if (!byName.has(c.Name.toLowerCase()))
441
- byName.set(c.Name.toLowerCase(), c);
442
- }
443
- return [...byName.values()];
444
- }
445
- // ─── TestConnection ──────────────────────────────────────────────
446
- async TestConnection(companyIntegration, contextUser) {
447
- try {
448
- const config = await this.ParseConfig(companyIntegration, contextUser);
449
- const token = await this.GetToken(config);
450
- // Verify token works by listing communities
451
- const response = await fetch(`${RASA_API_BASE}/communities`, {
452
- method: 'GET',
453
- headers: { 'rasa-token': token, 'Accept': 'application/json' },
454
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
455
- });
456
- if (!response.ok) {
457
- return {
458
- Success: false,
459
- Message: `Connection failed: HTTP ${response.status} from communities endpoint`,
460
- };
461
- }
462
- const body = await response.json();
463
- const results = body.results;
464
- const communityCount = results?.length ?? 0;
465
- return {
466
- Success: true,
467
- Message: `Connected to Rasa.io — ${communityCount} community(ies) accessible`,
468
- ServerVersion: 'Rasa.io API v1',
469
- };
470
- }
471
- catch (err) {
472
- const message = err instanceof Error ? err.message : String(err);
473
- return { Success: false, Message: `Connection failed: ${message}` };
474
- }
475
- }
476
- // ─── Default Configuration ───────────────────────────────────────
477
- GetDefaultConfiguration() {
478
- return {
479
- DefaultSchemaName: 'RasaIO',
480
- DefaultObjects: [],
481
- };
482
- }
483
- GetDefaultFieldMappings(objectName, _entityName) {
484
- const lowerName = objectName.toLowerCase();
485
- if (lowerName === 'persons' || lowerName === 'contacts') {
486
- return [
487
- { SourceFieldName: 'id', DestinationFieldName: 'ExternalID', IsKeyField: true },
488
- { SourceFieldName: 'email', DestinationFieldName: 'Email' },
489
- { SourceFieldName: 'first_name', DestinationFieldName: 'FirstName' },
490
- { SourceFieldName: 'last_name', DestinationFieldName: 'LastName' },
491
- { SourceFieldName: 'external_id', DestinationFieldName: 'SourceExternalID' },
492
- { SourceFieldName: 'is_active', DestinationFieldName: 'IsActive' },
493
- { SourceFieldName: 'is_subscribed', DestinationFieldName: 'IsSubscribed' },
494
- { SourceFieldName: 'created', DestinationFieldName: 'CreatedAt' },
495
- { SourceFieldName: 'updated', DestinationFieldName: 'UpdatedAt' },
496
- ];
497
- }
498
- if (lowerName === 'posts' || lowerName === 'content') {
499
- return [
500
- { SourceFieldName: 'id', DestinationFieldName: 'ExternalID', IsKeyField: true },
501
- { SourceFieldName: 'url', DestinationFieldName: 'URL' },
502
- { SourceFieldName: 'title', DestinationFieldName: 'Title' },
503
- { SourceFieldName: 'description', DestinationFieldName: 'Description' },
504
- { SourceFieldName: 'source_url', DestinationFieldName: 'SourceURL' },
505
- { SourceFieldName: 'image_url', DestinationFieldName: 'ImageURL' },
506
- { SourceFieldName: 'quality_score', DestinationFieldName: 'QualityScore' },
507
- { SourceFieldName: 'is_active', DestinationFieldName: 'IsActive' },
508
- { SourceFieldName: 'created', DestinationFieldName: 'CreatedAt' },
509
- { SourceFieldName: 'updated', DestinationFieldName: 'UpdatedAt' },
510
- ];
511
- }
512
- return [];
513
- }
514
- // ─── FetchChanges Override ────────────────────────────────────────
515
- /**
516
- * Override FetchChanges to:
517
- * 1. Store watermark and object name for use in BuildPaginatedURL/NormalizeResponse
518
- * 2. Advance watermark on final batch using latest record timestamp
519
- *
520
- * Watermark filtering is done server-side via updated_since/created_since params
521
- * (set in BuildPaginatedURL), so no client-side filtering is needed here.
522
- */
523
- async FetchChanges(ctx) {
524
- console.log(`[Rasa.io] FetchChanges called for '${ctx.ObjectName}' (batchSize=${ctx.BatchSize}, watermark=${ctx.WatermarkValue ?? 'none'}, offset=${ctx.CurrentOffset ?? 'none'}, cursor=${ctx.CurrentCursor ?? 'none'})`);
525
- // Reset counter and seen-IDs on the first page. Don't reset when serving from buffer
526
- // (buffer calls also have no offset/cursor but the buffer key will be present).
527
- const isFirstCall = !ctx.CurrentOffset && !ctx.CurrentPage && !ctx.CurrentCursor;
528
- if (isFirstCall && !this._batchBuffer.has(ctx.ObjectName)) {
529
- this._runningFetchTotal = 0;
530
- this._seenIDs.set(ctx.ObjectName, new Set());
531
- }
532
- // Store for use in BuildPaginatedURL and NormalizeResponse
533
- this._currentWatermark = ctx.WatermarkValue ?? undefined;
534
- this._currentObjectName = ctx.ObjectName ?? '';
535
- // Serve from buffer if a previous call left unconsumed records
536
- const buffered = this._batchBuffer.get(ctx.ObjectName);
537
- if (buffered && buffered.length > 0) {
538
- return this.ServeBufferedRecords(ctx.ObjectName, buffered, ctx.BatchSize);
539
- }
540
- // Analytics endpoints use POST with a JSON body — the generic GET-only
541
- // paginated loop in BaseRESTIntegrationConnector does not fit, so we
542
- // short-circuit here.
543
- if (ANALYTICS_POST_OBJECTS.has(ctx.ObjectName.toLowerCase())) {
544
- return this.FetchAnalyticsBatch(ctx);
545
- }
546
- const result = await super.FetchChanges(ctx);
547
- // Detect API wrap-around: track all seen IDs, stop when entire batch is duplicates
548
- const seen = this._seenIDs.get(ctx.ObjectName);
549
- if (seen && result.Records.length > 0) {
550
- const dupeCount = result.Records.filter(r => seen.has(r.ExternalID)).length;
551
- for (const r of result.Records)
552
- seen.add(r.ExternalID);
553
- if (dupeCount === result.Records.length) {
554
- return {
555
- Records: [],
556
- HasMore: false,
557
- NewWatermarkValue: this.FindLatestTimestamp(result.Records) ?? ctx.WatermarkValue ?? undefined,
558
- };
559
- }
560
- }
561
- // When a non-paginated endpoint returns more than BatchSize in one shot,
562
- // buffer the full set and serve it in chunks so no records are silently dropped.
563
- if (!result.HasMore && result.Records.length > ctx.BatchSize) {
564
- const watermark = this.FindLatestTimestamp(result.Records);
565
- this._batchBuffer.set(ctx.ObjectName, result.Records);
566
- this._batchBufferWatermarks.set(ctx.ObjectName, watermark);
567
- return this.ServeBufferedRecords(ctx.ObjectName, result.Records, ctx.BatchSize);
568
- }
569
- const isFinalBatch = !result.HasMore;
570
- const latestTimestamp = isFinalBatch ? this.FindLatestTimestamp(result.Records) : null;
571
- return {
572
- ...result,
573
- NewWatermarkValue: isFinalBatch ? (latestTimestamp ?? ctx.WatermarkValue ?? undefined) : undefined,
574
- };
575
- }
576
- /**
577
- * Serves records from the internal buffer in BatchSize-sized chunks.
578
- * Splices consumed records from the buffer array so subsequent calls advance correctly.
579
- * Emits NewWatermarkValue only on the final chunk.
580
- */
581
- ServeBufferedRecords(objectName, buffer, batchSize) {
582
- const batch = buffer.splice(0, batchSize);
583
- const hasMore = buffer.length > 0;
584
- let newWatermarkValue;
585
- if (!hasMore) {
586
- const stored = this._batchBufferWatermarks.get(objectName);
587
- newWatermarkValue = stored ?? undefined;
588
- this._batchBuffer.delete(objectName);
589
- this._batchBufferWatermarks.delete(objectName);
590
- }
591
- return {
592
- Records: batch,
593
- HasMore: hasMore,
594
- NewWatermarkValue: newWatermarkValue,
595
- };
596
- }
597
- // ─── Private Helpers ─────────────────────────────────────────────
598
- /**
599
- * Flattens the nested insights/topics response structure.
600
- * API returns one item per person with a topics[] array; we emit one row per (person, topic).
601
- */
602
- FlattenInsightsTopics(rawResults) {
603
- const rows = [];
604
- for (const item of rawResults) {
605
- // Unwrap 'data' envelope if present
606
- const record = (item['data'] && typeof item['data'] === 'object' && !Array.isArray(item['data']))
607
- ? item['data']
608
- : item;
609
- const personId = record['id'] ?? record['person_id'];
610
- const externalId = record['external_id'];
611
- const email = record['email'];
612
- const topics = record['topics'];
613
- if (!Array.isArray(topics))
614
- continue;
615
- for (const topicEntry of topics) {
616
- rows.push({
617
- person_id: personId,
618
- external_id: externalId,
619
- email,
620
- topic: topicEntry['topic'],
621
- first_click: topicEntry['first_click'],
622
- last_click: topicEntry['last_click'],
623
- weight: topicEntry['weight'],
624
- });
625
- }
626
- }
627
- this._runningFetchTotal += rows.length;
628
- console.log(`[Rasa.io] Flattened ${rows.length} insights/topics rows from ${rawResults.length} persons`);
629
- return rows;
630
- }
631
- /**
632
- * Flattens the /persons/{id}/attributes response into one row per (person, attribute).
633
- * Rasa.io does NOT expose a schema discovery endpoint for attributes — custom attributes
634
- * are free-form key-value pairs, so we emit each non-null key as a separate row.
635
- */
636
- FlattenPersonAttributes(rawResults) {
637
- const rows = [];
638
- for (const item of rawResults) {
639
- const record = (item['data'] && typeof item['data'] === 'object' && !Array.isArray(item['data']))
640
- ? item['data']
641
- : item;
642
- const personId = record['id'] ?? record['person_id'];
643
- const externalId = record['external_id'];
644
- const email = record['email'];
645
- const attributes = record['attributes'] ?? record['custom_attributes'];
646
- if (!attributes || typeof attributes !== 'object' || Array.isArray(attributes))
647
- continue;
648
- for (const [attrName, attrValue] of Object.entries(attributes)) {
649
- if (attrValue === undefined)
650
- continue;
651
- rows.push({
652
- person_id: personId,
653
- external_id: externalId,
654
- email,
655
- attribute_name: attrName,
656
- attribute_value: this.StringifyAttributeValue(attrValue),
657
- });
658
- }
659
- }
660
- this._runningFetchTotal += rows.length;
661
- console.log(`[Rasa.io] Flattened ${rows.length} person-attribute rows from ${rawResults.length} persons`);
662
- return rows;
663
- }
664
- /**
665
- * Flattens /analytics/activities date-histogram responses. Each result is a bucket
666
- * containing { bucket_start, <metric>: value, ... }. We emit one row per
667
- * (bucket_start, metric_name).
668
- */
669
- FlattenActivitiesAnalytics(rawResults) {
670
- const rows = [];
671
- for (const item of rawResults) {
672
- const record = (item['data'] && typeof item['data'] === 'object' && !Array.isArray(item['data']))
673
- ? item['data']
674
- : item;
675
- const bucketStart = (record['bucket_start'] ?? record['date'] ?? record['timestamp']);
676
- if (bucketStart == null)
677
- continue;
678
- // The body carries metric values keyed by name (opens, clicks, deliveries, bounces, ...)
679
- for (const [key, value] of Object.entries(record)) {
680
- if (key === 'bucket_start' || key === 'date' || key === 'timestamp')
681
- continue;
682
- if (key === 'segment_code' || key === 'interval' || key === 'timezone')
683
- continue;
684
- if (value == null || typeof value === 'object')
685
- continue;
686
- rows.push({
687
- bucket_start: String(bucketStart),
688
- metric_name: key,
689
- metric_value: typeof value === 'number' ? value : Number(value),
690
- segment_code: record['segment_code'] ?? null,
691
- interval: record['interval'] ?? null,
692
- timezone: record['timezone'] ?? null,
693
- });
694
- }
695
- }
696
- this._runningFetchTotal += rows.length;
697
- console.log(`[Rasa.io] Flattened ${rows.length} analytics-activities rows from ${rawResults.length} buckets`);
698
- return rows;
699
- }
700
- /** Stringifies a custom attribute value for nvarchar storage. */
701
- StringifyAttributeValue(value) {
702
- if (value == null)
703
- return null;
704
- if (typeof value === 'string')
705
- return value;
706
- if (typeof value === 'number' || typeof value === 'boolean')
707
- return String(value);
708
- try {
709
- return JSON.stringify(value);
710
- }
711
- catch {
712
- return null;
713
- }
714
- }
715
- /**
716
- * Fetches a batch from a Rasa analytics endpoint. These endpoints require POST
717
- * with a JSON body and are NOT paginated — the whole response is returned in
718
- * one call. We build a body that respects the caller's watermark (if any) and
719
- * delegate to the base pipeline for flattening/pagination-state resolution.
720
- */
721
- async FetchAnalyticsBatch(ctx) {
722
- const auth = await this.Authenticate(ctx.CompanyIntegration, ctx.ContextUser);
723
- const obj = this.GetCachedObject(ctx.CompanyIntegration.IntegrationID, ctx.ObjectName);
724
- const fields = this.GetCachedFields(obj.ID);
725
- const baseURL = this.GetBaseURL(ctx.CompanyIntegration);
726
- const url = `${baseURL.replace(/\/+$/, '')}${obj.APIPath.startsWith('/') ? obj.APIPath : `/${obj.APIPath}`}`;
727
- this._lastFetchedPath = obj.APIPath;
728
- const body = this.BuildAnalyticsBody(ctx.ObjectName, ctx.WatermarkValue, ctx.BatchSize ?? DEFAULT_PAGE_SIZE);
729
- const headers = this.BuildHeaders(auth);
730
- const response = await this.MakeHTTPRequest(auth, url, 'POST', headers, body);
731
- if (response.Status < 200 || response.Status >= 300) {
732
- console.warn(`[Rasa.io] Analytics POST ${url} returned HTTP ${response.Status}`);
733
- return { Records: [], HasMore: false };
734
- }
735
- const records = this.NormalizeResponse(response.Body, obj.ResponseDataKey);
736
- const pkFieldNames = fields.filter(f => f.IsPrimaryKey).map(f => f.Name);
737
- const pkNames = pkFieldNames.length > 0 ? pkFieldNames : ['id'];
738
- const externalRecords = records.map(r => ({
739
- ExternalID: pkNames.map(n => r[n] != null ? String(r[n]) : '').join('|'),
740
- ObjectType: ctx.ObjectName,
741
- Fields: r,
742
- }));
743
- // Advance watermark to 'now' — analytics endpoints don't return per-record timestamps.
744
- const nowIso = new Date().toISOString();
745
- return {
746
- Records: externalRecords,
747
- HasMore: false,
748
- NewWatermarkValue: nowIso,
749
- };
750
- }
751
- /**
752
- * Builds the POST body for an analytics endpoint. Watermark is translated into
753
- * an explicit `date_range.start_date` so that subsequent runs only pull new data.
754
- */
755
- BuildAnalyticsBody(objectName, watermark, limit) {
756
- const now = new Date();
757
- // If we have a watermark, use explicit date range from watermark to now;
758
- // otherwise fall back to the preset label.
759
- const useExplicitRange = typeof watermark === 'string' && watermark.length > 0;
760
- const dateRange = useExplicitRange
761
- ? {
762
- start_date: watermark,
763
- end_date: now.toISOString(),
764
- }
765
- : ANALYTICS_DEFAULT_DATE_RANGE;
766
- const commonBody = {
767
- date_range: dateRange,
768
- suspect_click: ANALYTICS_DEFAULT_SUSPECT_CLICK,
769
- segment_code: ANALYTICS_DEFAULT_SEGMENT,
770
- timezone: ANALYTICS_DEFAULT_TIMEZONE,
771
- limit,
772
- };
773
- if (objectName.toLowerCase() === 'analytics-activities') {
774
- commonBody['interval'] = ANALYTICS_DEFAULT_INTERVAL;
775
- }
776
- return commonBody;
777
- }
778
- /**
779
- * Parses connection config from CompanyIntegration credentials.
780
- * Tries CredentialID first (MJ: Credentials entity), then Configuration JSON.
781
- */
782
- async ParseConfig(companyIntegration, contextUser) {
783
- // Try CredentialID first
784
- if (companyIntegration.CredentialID) {
785
- return this.ParseConfigFromCredential(companyIntegration.CredentialID, contextUser);
786
- }
787
- // Fallback to Configuration JSON
788
- if (companyIntegration.Configuration) {
789
- return this.ParseConfigFromJSON(companyIntegration.Configuration);
790
- }
791
- throw new Error('Rasa.io connector requires either CredentialID or Configuration JSON');
792
- }
793
- /**
794
- * Loads credentials from the MJ: Credentials entity.
795
- */
796
- async ParseConfigFromCredential(credentialID, contextUser, provider) {
797
- const md = provider ?? new Metadata();
798
- const credEntity = await md.GetEntityObject('MJ: Credentials', contextUser);
799
- await credEntity.Load(credentialID);
800
- const valuesJSON = credEntity.Values;
801
- if (!valuesJSON) {
802
- throw new Error('Credential record has no Values JSON');
803
- }
804
- const values = JSON.parse(valuesJSON);
805
- return this.ExtractConfigFields(values);
806
- }
807
- /**
808
- * Parses config from the CompanyIntegration.Configuration JSON string.
809
- */
810
- ParseConfigFromJSON(configJSON) {
811
- const parsed = JSON.parse(configJSON);
812
- return this.ExtractConfigFields(parsed);
813
- }
814
- /**
815
- * Extracts config fields with case-insensitive key matching.
816
- */
817
- ExtractConfigFields(values) {
818
- const get = (key) => {
819
- const lower = key.toLowerCase();
820
- for (const [k, v] of Object.entries(values)) {
821
- if (k.toLowerCase() === lower)
822
- return v;
823
- }
824
- return undefined;
825
- };
826
- const apiKey = get('apikey') ?? get('api_key') ?? get('key');
827
- const username = get('username') ?? get('email') ?? get('user');
828
- const password = get('password') ?? get('pass');
829
- const communityIdentifier = get('communityidentifier') ?? get('community_identifier') ?? get('community');
830
- if (!apiKey) {
831
- throw new Error('Rasa.io configuration missing required field: APIKey');
832
- }
833
- if (!username || !password) {
834
- throw new Error('Rasa.io configuration missing required fields: Username and Password (for Basic auth)');
835
- }
836
- return {
837
- APIKey: apiKey,
838
- Username: username,
839
- Password: password,
840
- CommunityIdentifier: communityIdentifier,
841
- };
842
- }
843
- /**
844
- * Obtains a JWT token from Rasa.io, using cache if still valid.
845
- */
846
- async GetToken(config) {
847
- // Return cached token if still fresh
848
- if (this.cachedToken && (Date.now() - this.tokenObtainedAt) < TOKEN_TTL_MS) {
849
- return this.cachedToken;
850
- }
851
- const basicAuth = Buffer.from(`${config.Username}:${config.Password}`).toString('base64');
852
- const response = await fetch(`${RASA_API_BASE}/tokens`, {
853
- method: 'POST',
854
- headers: {
855
- 'Authorization': `Basic ${basicAuth}`,
856
- 'Content-Type': 'application/json',
857
- 'Accept': 'application/json',
858
- },
859
- body: JSON.stringify({ key: config.APIKey }),
860
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
861
- });
862
- if (!response.ok) {
863
- const body = await response.text();
864
- throw new Error(`Rasa.io token request failed (HTTP ${response.status}): ${body.slice(0, 500)}`);
865
- }
866
- const body = await response.json();
867
- // Token could be in results array or directly in response
868
- let token;
869
- if (body.results && Array.isArray(body.results) && body.results.length > 0) {
870
- const firstResult = body.results[0];
871
- token = (firstResult.token ?? firstResult['rasa-token'] ?? firstResult.jwt);
872
- }
873
- if (!token && typeof body.token === 'string') {
874
- token = body.token;
875
- }
876
- if (!token && typeof body['rasa-token'] === 'string') {
877
- token = body['rasa-token'];
878
- }
879
- if (!token) {
880
- throw new Error('Rasa.io token response did not contain a token');
881
- }
882
- this.cachedToken = token;
883
- this.tokenObtainedAt = Date.now();
884
- return token;
885
- }
886
- /**
887
- * Finds the latest timestamp from records for watermark advancement.
888
- * Uses 'created' for insights/actions (its only timestamp field); 'updated' for everything else.
889
- */
890
- FindLatestTimestamp(records) {
891
- const objectName = this._currentObjectName.toLowerCase();
892
- const primaryField = objectName.includes('action') ? 'created' : 'updated';
893
- const fallbackField = primaryField === 'updated' ? 'created' : 'updated';
894
- let latest = null;
895
- for (const record of records) {
896
- const dateStr = (record.Fields[primaryField] ?? record.Fields[fallbackField]);
897
- if (!dateStr)
898
- continue;
899
- const date = new Date(dateStr);
900
- if (!isNaN(date.getTime()) && (latest === null || date > latest)) {
901
- latest = date;
902
- }
903
- }
904
- return latest ? latest.toISOString() : null;
905
- }
906
- /**
907
- * Checks if an error is a timeout or network error.
908
- */
909
- IsTimeoutOrNetworkError(err) {
910
- if (err instanceof Error) {
911
- const msg = err.message.toLowerCase();
912
- return msg.includes('timeout') || msg.includes('abort') ||
913
- msg.includes('econnreset') || msg.includes('econnrefused') ||
914
- msg.includes('fetch failed');
915
- }
916
- return false;
917
- }
918
- /**
919
- * Sleeps for the specified number of milliseconds.
920
- */
921
- Sleep(ms) {
922
- return new Promise(resolve => setTimeout(resolve, ms));
923
- }
924
- };
925
- RasaConnector = __decorate([
926
- RegisterClass(BaseIntegrationConnector, 'RasaConnector')
927
- ], RasaConnector);
928
- export { RasaConnector };
929
- //# sourceMappingURL=RasaConnector.js.map