@memberjunction/integration-connectors 5.50.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.
- package/LICENSE +7 -0
- package/README.md +109 -277
- package/dist/index.d.ts +0 -36
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -38
- package/dist/index.js.map +1 -1
- package/package.json +14 -19
- package/dist/AptifyConnector.d.ts +0 -95
- package/dist/AptifyConnector.d.ts.map +0 -1
- package/dist/AptifyConnector.js +0 -760
- package/dist/AptifyConnector.js.map +0 -1
- package/dist/BlackbaudConnector.d.ts +0 -64
- package/dist/BlackbaudConnector.d.ts.map +0 -1
- package/dist/BlackbaudConnector.js +0 -916
- package/dist/BlackbaudConnector.js.map +0 -1
- package/dist/ConstantContactConnector.d.ts +0 -137
- package/dist/ConstantContactConnector.d.ts.map +0 -1
- package/dist/ConstantContactConnector.js +0 -663
- package/dist/ConstantContactConnector.js.map +0 -1
- package/dist/CventConnector.d.ts +0 -146
- package/dist/CventConnector.d.ts.map +0 -1
- package/dist/CventConnector.js +0 -540
- package/dist/CventConnector.js.map +0 -1
- package/dist/DynamicsDataverseConnector.d.ts +0 -296
- package/dist/DynamicsDataverseConnector.d.ts.map +0 -1
- package/dist/DynamicsDataverseConnector.js +0 -790
- package/dist/DynamicsDataverseConnector.js.map +0 -1
- package/dist/FileFeedConnector.d.ts +0 -64
- package/dist/FileFeedConnector.d.ts.map +0 -1
- package/dist/FileFeedConnector.js +0 -190
- package/dist/FileFeedConnector.js.map +0 -1
- package/dist/FontevaConnector.d.ts +0 -153
- package/dist/FontevaConnector.d.ts.map +0 -1
- package/dist/FontevaConnector.js +0 -421
- package/dist/FontevaConnector.js.map +0 -1
- package/dist/GrowthZoneConnector.d.ts +0 -183
- package/dist/GrowthZoneConnector.d.ts.map +0 -1
- package/dist/GrowthZoneConnector.js +0 -702
- package/dist/GrowthZoneConnector.js.map +0 -1
- package/dist/HivebriteConnector.d.ts +0 -151
- package/dist/HivebriteConnector.d.ts.map +0 -1
- package/dist/HivebriteConnector.js +0 -533
- package/dist/HivebriteConnector.js.map +0 -1
- package/dist/HubSpotConnector.d.ts +0 -523
- package/dist/HubSpotConnector.d.ts.map +0 -1
- package/dist/HubSpotConnector.js +0 -3110
- package/dist/HubSpotConnector.js.map +0 -1
- package/dist/IMISConnector.d.ts +0 -171
- package/dist/IMISConnector.d.ts.map +0 -1
- package/dist/IMISConnector.js +0 -695
- package/dist/IMISConnector.js.map +0 -1
- package/dist/MJToMJConnector.d.ts +0 -279
- package/dist/MJToMJConnector.d.ts.map +0 -1
- package/dist/MJToMJConnector.js +0 -893
- package/dist/MJToMJConnector.js.map +0 -1
- package/dist/MagnetMailConnector.d.ts +0 -163
- package/dist/MagnetMailConnector.d.ts.map +0 -1
- package/dist/MagnetMailConnector.js +0 -823
- package/dist/MagnetMailConnector.js.map +0 -1
- package/dist/MailchimpConnector.d.ts +0 -197
- package/dist/MailchimpConnector.d.ts.map +0 -1
- package/dist/MailchimpConnector.js +0 -791
- package/dist/MailchimpConnector.js.map +0 -1
- package/dist/MemberSuiteConnector.d.ts +0 -185
- package/dist/MemberSuiteConnector.d.ts.map +0 -1
- package/dist/MemberSuiteConnector.js +0 -762
- package/dist/MemberSuiteConnector.js.map +0 -1
- package/dist/NeonCRMConnector.d.ts +0 -273
- package/dist/NeonCRMConnector.d.ts.map +0 -1
- package/dist/NeonCRMConnector.js +0 -1039
- package/dist/NeonCRMConnector.js.map +0 -1
- package/dist/NetForumConnector.d.ts +0 -170
- package/dist/NetForumConnector.d.ts.map +0 -1
- package/dist/NetForumConnector.js +0 -778
- package/dist/NetForumConnector.js.map +0 -1
- package/dist/NetSuiteConnector.d.ts +0 -136
- package/dist/NetSuiteConnector.d.ts.map +0 -1
- package/dist/NetSuiteConnector.js +0 -703
- package/dist/NetSuiteConnector.js.map +0 -1
- package/dist/NimbleAMSConnector.d.ts +0 -142
- package/dist/NimbleAMSConnector.d.ts.map +0 -1
- package/dist/NimbleAMSConnector.js +0 -703
- package/dist/NimbleAMSConnector.js.map +0 -1
- package/dist/NoviConnector.d.ts +0 -178
- package/dist/NoviConnector.d.ts.map +0 -1
- package/dist/NoviConnector.js +0 -566
- package/dist/NoviConnector.js.map +0 -1
- package/dist/ORCIDConnector.d.ts +0 -207
- package/dist/ORCIDConnector.d.ts.map +0 -1
- package/dist/ORCIDConnector.js +0 -708
- package/dist/ORCIDConnector.js.map +0 -1
- package/dist/OpenWaterConnector.d.ts +0 -173
- package/dist/OpenWaterConnector.d.ts.map +0 -1
- package/dist/OpenWaterConnector.js +0 -893
- package/dist/OpenWaterConnector.js.map +0 -1
- package/dist/PathLMSConnector.d.ts +0 -395
- package/dist/PathLMSConnector.d.ts.map +0 -1
- package/dist/PathLMSConnector.js +0 -1281
- package/dist/PathLMSConnector.js.map +0 -1
- package/dist/PheedLoopConnector.d.ts +0 -119
- package/dist/PheedLoopConnector.d.ts.map +0 -1
- package/dist/PheedLoopConnector.js +0 -429
- package/dist/PheedLoopConnector.js.map +0 -1
- package/dist/PropFuelConnector.d.ts +0 -153
- package/dist/PropFuelConnector.d.ts.map +0 -1
- package/dist/PropFuelConnector.js +0 -450
- package/dist/PropFuelConnector.js.map +0 -1
- package/dist/QuickBooksConnector.d.ts +0 -92
- package/dist/QuickBooksConnector.d.ts.map +0 -1
- package/dist/QuickBooksConnector.js +0 -713
- package/dist/QuickBooksConnector.js.map +0 -1
- package/dist/RasaConnector.d.ts +0 -152
- package/dist/RasaConnector.d.ts.map +0 -1
- package/dist/RasaConnector.js +0 -929
- package/dist/RasaConnector.js.map +0 -1
- package/dist/Reach360Connector.d.ts +0 -175
- package/dist/Reach360Connector.d.ts.map +0 -1
- package/dist/Reach360Connector.js +0 -796
- package/dist/Reach360Connector.js.map +0 -1
- package/dist/RelationalDBConnector.d.ts +0 -108
- package/dist/RelationalDBConnector.d.ts.map +0 -1
- package/dist/RelationalDBConnector.js +0 -228
- package/dist/RelationalDBConnector.js.map +0 -1
- package/dist/RhythmConnector.d.ts +0 -188
- package/dist/RhythmConnector.d.ts.map +0 -1
- package/dist/RhythmConnector.js +0 -784
- package/dist/RhythmConnector.js.map +0 -1
- package/dist/SageIntacctConnector.d.ts +0 -337
- package/dist/SageIntacctConnector.d.ts.map +0 -1
- package/dist/SageIntacctConnector.js +0 -2081
- package/dist/SageIntacctConnector.js.map +0 -1
- package/dist/SalesforceConnector.d.ts +0 -461
- package/dist/SalesforceConnector.d.ts.map +0 -1
- package/dist/SalesforceConnector.js +0 -1862
- package/dist/SalesforceConnector.js.map +0 -1
- package/dist/SharePointConnector.d.ts +0 -188
- package/dist/SharePointConnector.d.ts.map +0 -1
- package/dist/SharePointConnector.js +0 -716
- package/dist/SharePointConnector.js.map +0 -1
- package/dist/WicketConnector.d.ts +0 -252
- package/dist/WicketConnector.d.ts.map +0 -1
- package/dist/WicketConnector.js +0 -938
- package/dist/WicketConnector.js.map +0 -1
- package/dist/WildApricotConnector.d.ts +0 -201
- package/dist/WildApricotConnector.d.ts.map +0 -1
- package/dist/WildApricotConnector.js +0 -1129
- package/dist/WildApricotConnector.js.map +0 -1
- package/dist/YourMembershipConnector.d.ts +0 -349
- package/dist/YourMembershipConnector.d.ts.map +0 -1
- package/dist/YourMembershipConnector.js +0 -4062
- package/dist/YourMembershipConnector.js.map +0 -1
- package/dist/generate-integration-actions.d.ts +0 -23
- package/dist/generate-integration-actions.d.ts.map +0 -1
- package/dist/generate-integration-actions.js +0 -201
- package/dist/generate-integration-actions.js.map +0 -1
package/dist/RasaConnector.js
DELETED
|
@@ -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
|