@memberjunction/integration-connectors 5.51.0 → 6.1.0-edge.1
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
|
@@ -1,663 +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
|
-
const CC_BASE_URL = 'https://api.cc.email/v3';
|
|
12
|
-
const CC_TOKEN_URL = 'https://authz.constantcontact.com/oauth2/default/v1/token';
|
|
13
|
-
const DEFAULT_MAX_RETRIES = 4;
|
|
14
|
-
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
15
|
-
/** Keeps us below the 4 req/sec rate limit (~250ms * 4 = 1 sec). */
|
|
16
|
-
const DEFAULT_MIN_REQUEST_INTERVAL_MS = 260;
|
|
17
|
-
const TOKEN_REFRESH_BUFFER_MS = 60_000;
|
|
18
|
-
// ─── Connector ─────────────────────────────────────────────────────────
|
|
19
|
-
/**
|
|
20
|
-
* Connector for the Constant Contact V3 REST API.
|
|
21
|
-
*
|
|
22
|
-
* Extends BaseRESTIntegrationConnector so the generic metadata-driven fetch
|
|
23
|
-
* loop handles pagination, template variables, and record normalization.
|
|
24
|
-
*
|
|
25
|
-
* Constant Contact-specific behavior:
|
|
26
|
-
* - OAuth 2.0 Authorization Code flow with rotating refresh tokens
|
|
27
|
-
* (the connector persists the newest refresh token after each refresh)
|
|
28
|
-
* - Cursor pagination via `_links.next.href` (full URL)
|
|
29
|
-
* - Incremental sync via `updated_after=<iso>` query param on list endpoints
|
|
30
|
-
* - Async bulk operations via the `/activities` endpoint family (read-only here;
|
|
31
|
-
* creation of activity jobs is exposed as separate actions)
|
|
32
|
-
*
|
|
33
|
-
* Rate limits: 10,000 calls/day and 4 calls/sec per account.
|
|
34
|
-
* Reference: https://developer.constantcontact.com/api_reference/
|
|
35
|
-
*/
|
|
36
|
-
let ConstantContactConnector = class ConstantContactConnector extends BaseRESTIntegrationConnector {
|
|
37
|
-
constructor() {
|
|
38
|
-
super(...arguments);
|
|
39
|
-
this.authState = null;
|
|
40
|
-
this.lastRequestTime = 0;
|
|
41
|
-
/** Current watermark for the in-flight FetchChanges — used by AppendDefaultQueryParams */
|
|
42
|
-
this.currentWatermark = null;
|
|
43
|
-
/** Cursor URL returned by the previous page — keeps the base class cursor loop happy. */
|
|
44
|
-
this.lastNextCursorUrl = null;
|
|
45
|
-
}
|
|
46
|
-
// ── Capability flags ────────────────────────────────────────────
|
|
47
|
-
get SupportsCreate() { return true; }
|
|
48
|
-
get SupportsUpdate() { return true; }
|
|
49
|
-
get SupportsDelete() { return true; }
|
|
50
|
-
get SupportsSearch() { return false; }
|
|
51
|
-
get SupportsListing() { return false; }
|
|
52
|
-
get IntegrationName() { return 'Constant Contact'; }
|
|
53
|
-
// ── Abstract method implementations ─────────────────────────────
|
|
54
|
-
async Authenticate(companyIntegration, contextUser) {
|
|
55
|
-
return this.GetAuth(companyIntegration, contextUser);
|
|
56
|
-
}
|
|
57
|
-
BuildHeaders(auth) {
|
|
58
|
-
const ccAuth = auth;
|
|
59
|
-
return {
|
|
60
|
-
'Authorization': `Bearer ${ccAuth.Token}`,
|
|
61
|
-
'Accept': 'application/json',
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
async MakeHTTPRequest(auth, url, method, headers, body) {
|
|
65
|
-
const ccAuth = auth;
|
|
66
|
-
const maxRetries = ccAuth.Config.MaxRetries ?? DEFAULT_MAX_RETRIES;
|
|
67
|
-
const timeoutMs = ccAuth.Config.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
68
|
-
const minInterval = ccAuth.Config.MinRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS;
|
|
69
|
-
const effectiveHeaders = { ...headers };
|
|
70
|
-
if (body !== undefined && !effectiveHeaders['Content-Type']) {
|
|
71
|
-
effectiveHeaders['Content-Type'] = 'application/json';
|
|
72
|
-
}
|
|
73
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
74
|
-
await this.Throttle(minInterval);
|
|
75
|
-
const response = await this.ExecuteFetch(url, method, effectiveHeaders, body, timeoutMs);
|
|
76
|
-
this.lastRequestTime = Date.now();
|
|
77
|
-
if (response.status === 401 && attempt < maxRetries) {
|
|
78
|
-
this.authState = null;
|
|
79
|
-
const fresh = await this.GetAuth(ccAuth.Config);
|
|
80
|
-
ccAuth.Token = fresh.Token;
|
|
81
|
-
ccAuth.ExpiresAt = fresh.ExpiresAt;
|
|
82
|
-
effectiveHeaders['Authorization'] = `Bearer ${fresh.Token}`;
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
if (this.ShouldRetry(response.status) && attempt < maxRetries) {
|
|
86
|
-
const delay = this.ComputeRetryDelay(response, attempt);
|
|
87
|
-
await this.Sleep(delay);
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
return this.BuildRESTResponse(response);
|
|
91
|
-
}
|
|
92
|
-
throw new Error(`Constant Contact API request failed after ${maxRetries} attempt(s): ${url}`);
|
|
93
|
-
}
|
|
94
|
-
NormalizeResponse(rawBody, responseDataKey) {
|
|
95
|
-
if (rawBody == null)
|
|
96
|
-
return [];
|
|
97
|
-
// Capture cursor link for the next page, regardless of data key
|
|
98
|
-
if (typeof rawBody === 'object' && !Array.isArray(rawBody)) {
|
|
99
|
-
const record = rawBody;
|
|
100
|
-
const links = record['_links'];
|
|
101
|
-
this.lastNextCursorUrl = links?.next?.href ?? null;
|
|
102
|
-
if (responseDataKey && Array.isArray(record[responseDataKey])) {
|
|
103
|
-
return record[responseDataKey];
|
|
104
|
-
}
|
|
105
|
-
// Single-object response (e.g., GET /contacts/{id})
|
|
106
|
-
if (!responseDataKey) {
|
|
107
|
-
return [record];
|
|
108
|
-
}
|
|
109
|
-
return [];
|
|
110
|
-
}
|
|
111
|
-
if (Array.isArray(rawBody)) {
|
|
112
|
-
this.lastNextCursorUrl = null;
|
|
113
|
-
return rawBody;
|
|
114
|
-
}
|
|
115
|
-
return [];
|
|
116
|
-
}
|
|
117
|
-
ExtractPaginationInfo(_rawBody, paginationType, _currentPage, _currentOffset, _pageSize) {
|
|
118
|
-
if (paginationType === 'None')
|
|
119
|
-
return { HasMore: false };
|
|
120
|
-
const nextUrl = this.lastNextCursorUrl;
|
|
121
|
-
if (!nextUrl)
|
|
122
|
-
return { HasMore: false };
|
|
123
|
-
return { HasMore: true, NextCursor: nextUrl };
|
|
124
|
-
}
|
|
125
|
-
GetBaseURL(_companyIntegration, _auth) {
|
|
126
|
-
return CC_BASE_URL;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Constant Contact pagination uses a full `_links.next.href` URL. When the
|
|
130
|
-
* engine supplies a cursor that is already an absolute URL we honor it as-is;
|
|
131
|
-
* otherwise we append the `limit` query param to the base path.
|
|
132
|
-
*/
|
|
133
|
-
BuildPaginatedURL(basePath, obj, _page, _offset, cursor, effectivePageSize) {
|
|
134
|
-
if (cursor && this.IsAbsoluteURL(cursor)) {
|
|
135
|
-
return cursor;
|
|
136
|
-
}
|
|
137
|
-
const pageSize = effectivePageSize ?? obj.DefaultPageSize ?? 50;
|
|
138
|
-
const separator = basePath.includes('?') ? '&' : '?';
|
|
139
|
-
return `${basePath}${separator}limit=${pageSize}`;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Inject watermark filter (updated_after=<iso>) when an incremental sync is active.
|
|
143
|
-
* Skip the injection when the URL is already an absolute cursor URL (the
|
|
144
|
-
* cursor preserves the original filter state on subsequent pages).
|
|
145
|
-
*/
|
|
146
|
-
AppendDefaultQueryParams(url, obj) {
|
|
147
|
-
if (this.IsAbsoluteCursorURL(url))
|
|
148
|
-
return url;
|
|
149
|
-
let result = super.AppendDefaultQueryParams(url, obj);
|
|
150
|
-
if (this.currentWatermark && obj.SupportsIncrementalSync) {
|
|
151
|
-
const separator = result.includes('?') ? '&' : '?';
|
|
152
|
-
result = `${result}${separator}updated_after=${encodeURIComponent(this.currentWatermark)}`;
|
|
153
|
-
}
|
|
154
|
-
return result;
|
|
155
|
-
}
|
|
156
|
-
// ── Public API: TestConnection, FetchChanges, CRUD ──────────────
|
|
157
|
-
async TestConnection(companyIntegration, contextUser) {
|
|
158
|
-
try {
|
|
159
|
-
const auth = await this.GetAuth(companyIntegration, contextUser, true);
|
|
160
|
-
const url = `${auth.BaseUrl}/account/summary`;
|
|
161
|
-
const headers = this.BuildHeaders(auth);
|
|
162
|
-
const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
|
|
163
|
-
if (response.Status >= 200 && response.Status < 300) {
|
|
164
|
-
return {
|
|
165
|
-
Success: true,
|
|
166
|
-
Message: 'Connected to Constant Contact V3 API',
|
|
167
|
-
ServerVersion: 'Constant Contact V3',
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
return { Success: false, Message: `Constant Contact responded HTTP ${response.Status}` };
|
|
171
|
-
}
|
|
172
|
-
catch (err) {
|
|
173
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
174
|
-
return { Success: false, Message: `Connection failed: ${message}` };
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Canonical Constant Contact v3 API top-level objects. Returns the
|
|
179
|
-
* vendor-documented standard catalog (merged with anything already
|
|
180
|
-
* persisted in IntegrationObject via super.DiscoverObjects so operator-
|
|
181
|
-
* curated custom rows aren't lost). CC's schema is fixed — there are no
|
|
182
|
-
* "custom tables" to surface, but the per-account custom-fields ARE
|
|
183
|
-
* discovered via DiscoverFields('Contacts') which queries the live
|
|
184
|
-
* `/contact_custom_fields` endpoint and merges them into the Contacts
|
|
185
|
-
* field set.
|
|
186
|
-
*/
|
|
187
|
-
async DiscoverObjects(companyIntegration, contextUser) {
|
|
188
|
-
const persisted = await super.DiscoverObjects(companyIntegration, contextUser);
|
|
189
|
-
const canonical = [
|
|
190
|
-
{ Name: 'Contacts', Label: 'Contacts', Description: 'Contact records (people on mailing lists). v3 API /contacts.', SupportsIncrementalSync: true, SupportsWrite: true },
|
|
191
|
-
{ Name: 'ContactLists', Label: 'Contact Lists', Description: 'Mailing lists. v3 API /contact_lists.', SupportsIncrementalSync: true, SupportsWrite: true },
|
|
192
|
-
{ Name: 'ContactCustomFields', Label: 'Contact Custom Fields', Description: 'Account-defined custom fields available on Contacts. v3 API /contact_custom_fields.', SupportsIncrementalSync: false, SupportsWrite: true },
|
|
193
|
-
{ Name: 'ContactTags', Label: 'Contact Tags', Description: 'Account-defined contact tags. v3 API /contact_tags.', SupportsIncrementalSync: false, SupportsWrite: true },
|
|
194
|
-
{ Name: 'EmailCampaigns', Label: 'Email Campaigns', Description: 'Email campaigns. v3 API /emails.', SupportsIncrementalSync: true, SupportsWrite: true },
|
|
195
|
-
{ Name: 'EmailCampaignActivities', Label: 'Email Campaign Activities', Description: 'Per-campaign send/open/click activity. v3 API /emails/activities.', SupportsIncrementalSync: true, SupportsWrite: false },
|
|
196
|
-
{ Name: 'Segments', Label: 'Segments', Description: 'Audience segments. v3 API /segments.', SupportsIncrementalSync: true, SupportsWrite: true },
|
|
197
|
-
{ Name: 'Activities', Label: 'Activities', Description: 'Bulk activity records (contact import jobs, etc). v3 API /activities.', SupportsIncrementalSync: true, SupportsWrite: false },
|
|
198
|
-
];
|
|
199
|
-
const byName = new Map();
|
|
200
|
-
for (const o of persisted)
|
|
201
|
-
byName.set(o.Name.toLowerCase(), o);
|
|
202
|
-
for (const c of canonical) {
|
|
203
|
-
if (!byName.has(c.Name.toLowerCase()))
|
|
204
|
-
byName.set(c.Name.toLowerCase(), c);
|
|
205
|
-
}
|
|
206
|
-
return [...byName.values()];
|
|
207
|
-
}
|
|
208
|
-
async DiscoverFields(companyIntegration, objectName, contextUser) {
|
|
209
|
-
const staticFields = await super.DiscoverFields(companyIntegration, objectName, contextUser);
|
|
210
|
-
// For Contacts, custom fields are account-defined. Runtime-discover them
|
|
211
|
-
// so mapping UIs can present them even if they aren't in static metadata.
|
|
212
|
-
if (objectName.toLowerCase() === 'contacts' || objectName.toLowerCase() === 'contact') {
|
|
213
|
-
try {
|
|
214
|
-
const customFieldDefs = await this.FetchCustomFieldDefinitions(companyIntegration, contextUser);
|
|
215
|
-
const customFields = customFieldDefs.map(cf => this.CustomFieldToSchema(cf));
|
|
216
|
-
return this.MergeFieldSchemas(staticFields, customFields);
|
|
217
|
-
}
|
|
218
|
-
catch (err) {
|
|
219
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
220
|
-
console.warn(`[Constant Contact] Custom field discovery failed: ${message}`);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return staticFields;
|
|
224
|
-
}
|
|
225
|
-
async FetchChanges(ctx) {
|
|
226
|
-
this.currentWatermark = ctx.WatermarkValue;
|
|
227
|
-
this.lastNextCursorUrl = null;
|
|
228
|
-
try {
|
|
229
|
-
const result = await super.FetchChanges(ctx);
|
|
230
|
-
result.NewWatermarkValue = this.ComputeNewWatermark(result.Records, ctx.WatermarkValue);
|
|
231
|
-
return result;
|
|
232
|
-
}
|
|
233
|
-
finally {
|
|
234
|
-
this.currentWatermark = null;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
// ── Write operations ────────────────────────────────────────────
|
|
238
|
-
async CreateRecord(ctx) {
|
|
239
|
-
const companyIntegration = ctx.CompanyIntegration;
|
|
240
|
-
const contextUser = ctx.ContextUser;
|
|
241
|
-
const auth = await this.GetAuth(companyIntegration, contextUser);
|
|
242
|
-
const path = this.ResolveCRUDPath(ctx.ObjectName);
|
|
243
|
-
const url = `${auth.BaseUrl}${path}`;
|
|
244
|
-
const headers = this.BuildHeaders(auth);
|
|
245
|
-
const response = await this.MakeHTTPRequest(auth, url, 'POST', headers, ctx.Attributes);
|
|
246
|
-
if (response.Status >= 200 && response.Status < 300) {
|
|
247
|
-
const body = response.Body;
|
|
248
|
-
return this.BuildCreatedResult(this.ExtractIdFromRecord(body, ctx.ObjectName), response.Status, ctx.ObjectName);
|
|
249
|
-
}
|
|
250
|
-
return this.BuildCRUDError(response, 'CreateRecord', ctx.ObjectName);
|
|
251
|
-
}
|
|
252
|
-
async UpdateRecord(ctx) {
|
|
253
|
-
const companyIntegration = ctx.CompanyIntegration;
|
|
254
|
-
const contextUser = ctx.ContextUser;
|
|
255
|
-
const auth = await this.GetAuth(companyIntegration, contextUser);
|
|
256
|
-
const path = this.ResolveCRUDPath(ctx.ObjectName);
|
|
257
|
-
const url = `${auth.BaseUrl}${path}/${encodeURIComponent(ctx.ExternalID)}`;
|
|
258
|
-
const headers = this.BuildHeaders(auth);
|
|
259
|
-
const method = this.PreferredUpdateMethod(ctx.ObjectName);
|
|
260
|
-
const response = await this.MakeHTTPRequest(auth, url, method, headers, ctx.Attributes);
|
|
261
|
-
if (response.Status >= 200 && response.Status < 300) {
|
|
262
|
-
return { Success: true, ExternalID: ctx.ExternalID, StatusCode: response.Status };
|
|
263
|
-
}
|
|
264
|
-
return this.BuildCRUDError(response, 'UpdateRecord', ctx.ObjectName);
|
|
265
|
-
}
|
|
266
|
-
async DeleteRecord(ctx) {
|
|
267
|
-
const companyIntegration = ctx.CompanyIntegration;
|
|
268
|
-
const contextUser = ctx.ContextUser;
|
|
269
|
-
const auth = await this.GetAuth(companyIntegration, contextUser);
|
|
270
|
-
const path = this.ResolveCRUDPath(ctx.ObjectName);
|
|
271
|
-
const url = `${auth.BaseUrl}${path}/${encodeURIComponent(ctx.ExternalID)}`;
|
|
272
|
-
const headers = this.BuildHeaders(auth);
|
|
273
|
-
const response = await this.MakeHTTPRequest(auth, url, 'DELETE', headers);
|
|
274
|
-
if (response.Status >= 200 && response.Status < 300) {
|
|
275
|
-
return { Success: true, ExternalID: ctx.ExternalID, StatusCode: response.Status };
|
|
276
|
-
}
|
|
277
|
-
return this.BuildCRUDError(response, 'DeleteRecord', ctx.ObjectName);
|
|
278
|
-
}
|
|
279
|
-
async GetRecord(ctx) {
|
|
280
|
-
const companyIntegration = ctx.CompanyIntegration;
|
|
281
|
-
const contextUser = ctx.ContextUser;
|
|
282
|
-
const auth = await this.GetAuth(companyIntegration, contextUser);
|
|
283
|
-
const path = this.ResolveCRUDPath(ctx.ObjectName);
|
|
284
|
-
const url = `${auth.BaseUrl}${path}/${encodeURIComponent(ctx.ExternalID)}`;
|
|
285
|
-
const headers = this.BuildHeaders(auth);
|
|
286
|
-
const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
|
|
287
|
-
if (response.Status < 200 || response.Status >= 300)
|
|
288
|
-
return null;
|
|
289
|
-
if (response.Body == null || typeof response.Body !== 'object')
|
|
290
|
-
return null;
|
|
291
|
-
return {
|
|
292
|
-
ExternalID: ctx.ExternalID,
|
|
293
|
-
ObjectType: ctx.ObjectName,
|
|
294
|
-
Fields: response.Body,
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
// ── Default configuration / field mappings ──────────────────────
|
|
298
|
-
GetDefaultConfiguration() {
|
|
299
|
-
return {
|
|
300
|
-
DefaultSchemaName: 'ConstantContact',
|
|
301
|
-
DefaultObjects: [],
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
GetDefaultFieldMappings(objectName, _entityName) {
|
|
305
|
-
switch (objectName) {
|
|
306
|
-
case 'Contacts':
|
|
307
|
-
return [
|
|
308
|
-
{ SourceFieldName: 'contact_id', DestinationFieldName: 'ExternalID', IsKeyField: true },
|
|
309
|
-
{ SourceFieldName: 'email_address', DestinationFieldName: 'Email' },
|
|
310
|
-
{ SourceFieldName: 'first_name', DestinationFieldName: 'FirstName' },
|
|
311
|
-
{ SourceFieldName: 'last_name', DestinationFieldName: 'LastName' },
|
|
312
|
-
{ SourceFieldName: 'company_name', DestinationFieldName: 'CompanyName' },
|
|
313
|
-
{ SourceFieldName: 'job_title', DestinationFieldName: 'JobTitle' },
|
|
314
|
-
{ SourceFieldName: 'created_at', DestinationFieldName: 'CreatedAt' },
|
|
315
|
-
{ SourceFieldName: 'updated_at', DestinationFieldName: 'UpdatedAt' },
|
|
316
|
-
];
|
|
317
|
-
case 'ContactLists':
|
|
318
|
-
return [
|
|
319
|
-
{ SourceFieldName: 'list_id', DestinationFieldName: 'ExternalID', IsKeyField: true },
|
|
320
|
-
{ SourceFieldName: 'name', DestinationFieldName: 'Name' },
|
|
321
|
-
{ SourceFieldName: 'description', DestinationFieldName: 'Description' },
|
|
322
|
-
{ SourceFieldName: 'membership_count', DestinationFieldName: 'MembershipCount' },
|
|
323
|
-
];
|
|
324
|
-
case 'EmailCampaigns':
|
|
325
|
-
return [
|
|
326
|
-
{ SourceFieldName: 'campaign_id', DestinationFieldName: 'ExternalID', IsKeyField: true },
|
|
327
|
-
{ SourceFieldName: 'name', DestinationFieldName: 'Name' },
|
|
328
|
-
{ SourceFieldName: 'current_status', DestinationFieldName: 'Status' },
|
|
329
|
-
{ SourceFieldName: 'type', DestinationFieldName: 'CampaignType' },
|
|
330
|
-
{ SourceFieldName: 'created_at', DestinationFieldName: 'CreatedAt' },
|
|
331
|
-
{ SourceFieldName: 'updated_at', DestinationFieldName: 'UpdatedAt' },
|
|
332
|
-
];
|
|
333
|
-
default:
|
|
334
|
-
return [];
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
// ─────────────────────────────────────────────────────────────────
|
|
338
|
-
// Private helpers
|
|
339
|
-
// ─────────────────────────────────────────────────────────────────
|
|
340
|
-
// ── Auth helpers ────────────────────────────────────────────────
|
|
341
|
-
async GetAuth(source, contextUser, forceRefresh = false) {
|
|
342
|
-
if (!forceRefresh && this.authState && this.IsTokenValid(this.authState)) {
|
|
343
|
-
return this.authState;
|
|
344
|
-
}
|
|
345
|
-
const config = this.IsConfig(source)
|
|
346
|
-
? source
|
|
347
|
-
: await this.ParseConfig(source, contextUser);
|
|
348
|
-
this.authState = await this.RefreshAccessToken(config, source, contextUser);
|
|
349
|
-
return this.authState;
|
|
350
|
-
}
|
|
351
|
-
IsConfig(src) {
|
|
352
|
-
return src.ClientId !== undefined
|
|
353
|
-
&& src.RefreshToken !== undefined;
|
|
354
|
-
}
|
|
355
|
-
IsTokenValid(auth) {
|
|
356
|
-
return auth.ExpiresAt.getTime() > Date.now() + TOKEN_REFRESH_BUFFER_MS;
|
|
357
|
-
}
|
|
358
|
-
async RefreshAccessToken(config, companyIntegration, contextUser) {
|
|
359
|
-
const basicAuth = Buffer.from(`${config.ClientId}:${config.ClientSecret}`).toString('base64');
|
|
360
|
-
const body = new URLSearchParams();
|
|
361
|
-
body.set('grant_type', 'refresh_token');
|
|
362
|
-
body.set('refresh_token', config.RefreshToken);
|
|
363
|
-
const response = await fetch(CC_TOKEN_URL, {
|
|
364
|
-
method: 'POST',
|
|
365
|
-
headers: {
|
|
366
|
-
'Authorization': `Basic ${basicAuth}`,
|
|
367
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
368
|
-
'Accept': 'application/json',
|
|
369
|
-
},
|
|
370
|
-
body: body.toString(),
|
|
371
|
-
});
|
|
372
|
-
if (!response.ok) {
|
|
373
|
-
const text = await response.text();
|
|
374
|
-
throw new Error(`Constant Contact token refresh failed (${response.status}): ${text.slice(0, 300)}`);
|
|
375
|
-
}
|
|
376
|
-
const payload = await response.json();
|
|
377
|
-
config.AccessToken = payload.access_token;
|
|
378
|
-
// Refresh tokens rotate — persist the newest one or auth will break on next refresh.
|
|
379
|
-
config.RefreshToken = payload.refresh_token;
|
|
380
|
-
const expiresAt = new Date(Date.now() + (payload.expires_in ?? 86_400) * 1000);
|
|
381
|
-
// Best-effort: persist the rotated refresh token back to the Credential record.
|
|
382
|
-
if (companyIntegration?.CredentialID && contextUser) {
|
|
383
|
-
await this.PersistRotatedRefreshToken(companyIntegration.CredentialID, contextUser, config);
|
|
384
|
-
}
|
|
385
|
-
return {
|
|
386
|
-
Token: payload.access_token,
|
|
387
|
-
ExpiresAt: expiresAt,
|
|
388
|
-
Config: config,
|
|
389
|
-
BaseUrl: CC_BASE_URL,
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
/**
|
|
393
|
-
* Persist the newly-rotated refresh token back to the MJ: Credentials record
|
|
394
|
-
* so the next cold-start refresh uses the correct token.
|
|
395
|
-
*/
|
|
396
|
-
async PersistRotatedRefreshToken(credentialID, contextUser, config, provider) {
|
|
397
|
-
try {
|
|
398
|
-
const md = provider ?? new Metadata();
|
|
399
|
-
const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
|
|
400
|
-
const loaded = await credential.Load(credentialID);
|
|
401
|
-
if (!loaded || !credential.Values)
|
|
402
|
-
return;
|
|
403
|
-
const parsed = JSON.parse(credential.Values);
|
|
404
|
-
const updated = {
|
|
405
|
-
...parsed,
|
|
406
|
-
RefreshToken: config.RefreshToken,
|
|
407
|
-
};
|
|
408
|
-
if (config.AccessToken)
|
|
409
|
-
updated.AccessToken = config.AccessToken;
|
|
410
|
-
credential.Values = JSON.stringify(updated);
|
|
411
|
-
const saved = await credential.Save();
|
|
412
|
-
if (!saved) {
|
|
413
|
-
const detail = credential.LatestResult?.CompleteMessage ?? 'unknown';
|
|
414
|
-
console.warn(`[Constant Contact] Failed to persist rotated refresh token: ${detail}`);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
catch (err) {
|
|
418
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
419
|
-
console.warn(`[Constant Contact] Refresh-token persistence error: ${message}`);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
// ── Configuration parsing ───────────────────────────────────────
|
|
423
|
-
async ParseConfig(companyIntegration, contextUser) {
|
|
424
|
-
if (companyIntegration.CredentialID) {
|
|
425
|
-
const config = await this.ParseConfigFromCredential(companyIntegration.CredentialID, contextUser);
|
|
426
|
-
if (config)
|
|
427
|
-
return config;
|
|
428
|
-
}
|
|
429
|
-
if (companyIntegration.Configuration) {
|
|
430
|
-
const parsed = JSON.parse(companyIntegration.Configuration);
|
|
431
|
-
return this.ExtractConfig(parsed);
|
|
432
|
-
}
|
|
433
|
-
throw new Error('Constant Contact connector requires either CredentialID or Configuration JSON');
|
|
434
|
-
}
|
|
435
|
-
async ParseConfigFromCredential(credentialID, contextUser, provider) {
|
|
436
|
-
const md = provider ?? new Metadata();
|
|
437
|
-
const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
|
|
438
|
-
const loaded = await credential.Load(credentialID);
|
|
439
|
-
if (!loaded || !credential.Values)
|
|
440
|
-
return null;
|
|
441
|
-
const values = JSON.parse(credential.Values);
|
|
442
|
-
return this.ExtractConfig(values);
|
|
443
|
-
}
|
|
444
|
-
ExtractConfig(values) {
|
|
445
|
-
const get = (...keys) => {
|
|
446
|
-
for (const key of keys) {
|
|
447
|
-
const hit = Object.entries(values).find(([k]) => k.toLowerCase() === key.toLowerCase());
|
|
448
|
-
if (hit)
|
|
449
|
-
return String(hit[1] ?? '');
|
|
450
|
-
}
|
|
451
|
-
return undefined;
|
|
452
|
-
};
|
|
453
|
-
const clientId = get('ClientId', 'clientId', 'client_id', 'apiKey', 'api_key');
|
|
454
|
-
const clientSecret = get('ClientSecret', 'clientSecret', 'client_secret', 'appSecret', 'app_secret');
|
|
455
|
-
const refreshToken = get('RefreshToken', 'refreshToken', 'refresh_token');
|
|
456
|
-
if (!clientId)
|
|
457
|
-
throw new Error('Constant Contact configuration missing required field: ClientId');
|
|
458
|
-
if (!clientSecret)
|
|
459
|
-
throw new Error('Constant Contact configuration missing required field: ClientSecret');
|
|
460
|
-
if (!refreshToken)
|
|
461
|
-
throw new Error('Constant Contact configuration missing required field: RefreshToken');
|
|
462
|
-
return {
|
|
463
|
-
ClientId: clientId,
|
|
464
|
-
ClientSecret: clientSecret,
|
|
465
|
-
RefreshToken: refreshToken,
|
|
466
|
-
AccessToken: get('AccessToken', 'accessToken', 'access_token'),
|
|
467
|
-
RedirectUri: get('RedirectUri', 'redirectUri', 'redirect_uri'),
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
// ── Custom field discovery ──────────────────────────────────────
|
|
471
|
-
async FetchCustomFieldDefinitions(companyIntegration, contextUser) {
|
|
472
|
-
const auth = await this.GetAuth(companyIntegration, contextUser);
|
|
473
|
-
const url = `${auth.BaseUrl}/contact_custom_fields?limit=100`;
|
|
474
|
-
const headers = this.BuildHeaders(auth);
|
|
475
|
-
const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
|
|
476
|
-
if (response.Status < 200 || response.Status >= 300)
|
|
477
|
-
return [];
|
|
478
|
-
const body = response.Body;
|
|
479
|
-
const arr = body['custom_fields'];
|
|
480
|
-
return Array.isArray(arr) ? arr : [];
|
|
481
|
-
}
|
|
482
|
-
CustomFieldToSchema(cf) {
|
|
483
|
-
const name = String(cf['name'] ?? cf['label'] ?? 'custom_field');
|
|
484
|
-
const label = String(cf['label'] ?? name);
|
|
485
|
-
const type = String(cf['type'] ?? 'string');
|
|
486
|
-
return {
|
|
487
|
-
Name: `custom.${name}`,
|
|
488
|
-
Label: `Custom: ${label}`,
|
|
489
|
-
Description: `Account custom field (type: ${type})`,
|
|
490
|
-
DataType: this.MapCustomFieldType(type),
|
|
491
|
-
IsRequired: false,
|
|
492
|
-
IsUniqueKey: false,
|
|
493
|
-
IsReadOnly: false,
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
MapCustomFieldType(type) {
|
|
497
|
-
switch (type.toLowerCase()) {
|
|
498
|
-
case 'number': return 'decimal';
|
|
499
|
-
case 'boolean': return 'boolean';
|
|
500
|
-
case 'date': return 'date';
|
|
501
|
-
case 'datetime': return 'datetime';
|
|
502
|
-
case 'currency': return 'decimal';
|
|
503
|
-
default: return 'string';
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
MergeFieldSchemas(staticFields, extra) {
|
|
507
|
-
const staticNames = new Set(staticFields.map(f => f.Name.toLowerCase()));
|
|
508
|
-
return [...staticFields, ...extra.filter(f => !staticNames.has(f.Name.toLowerCase()))];
|
|
509
|
-
}
|
|
510
|
-
// ── Path + response helpers ─────────────────────────────────────
|
|
511
|
-
/**
|
|
512
|
-
* Map MJ IntegrationObject names to Constant Contact REST paths for CRUD.
|
|
513
|
-
* Object names follow the MJ naming (PascalCase) while the API paths use snake_case.
|
|
514
|
-
*/
|
|
515
|
-
ResolveCRUDPath(objectName) {
|
|
516
|
-
switch (objectName) {
|
|
517
|
-
case 'Contacts': return '/contacts';
|
|
518
|
-
case 'ContactLists': return '/contact_lists';
|
|
519
|
-
case 'ContactCustomFields': return '/contact_custom_fields';
|
|
520
|
-
case 'ContactTags': return '/contact_tags';
|
|
521
|
-
case 'EmailCampaigns': return '/emails';
|
|
522
|
-
case 'EmailCampaignActivities': return '/emails/activities';
|
|
523
|
-
case 'Segments': return '/segments';
|
|
524
|
-
case 'Activities': return '/activities';
|
|
525
|
-
default: return `/${objectName}`;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
/**
|
|
529
|
-
* Some Constant Contact resources accept PATCH for partial updates; most
|
|
530
|
-
* require PUT for full replace. Default to PUT and promote the handful of
|
|
531
|
-
* resources that are known to support PATCH.
|
|
532
|
-
*/
|
|
533
|
-
PreferredUpdateMethod(objectName) {
|
|
534
|
-
const patchSupported = new Set(['ContactCustomFields', 'Segments', 'ContactTags']);
|
|
535
|
-
return patchSupported.has(objectName) ? 'PATCH' : 'PUT';
|
|
536
|
-
}
|
|
537
|
-
ExtractIdFromRecord(body, objectName) {
|
|
538
|
-
// Each Constant Contact resource has a named PK; try the most likely ones first.
|
|
539
|
-
const order = {
|
|
540
|
-
Contacts: ['contact_id'],
|
|
541
|
-
ContactLists: ['list_id'],
|
|
542
|
-
ContactCustomFields: ['custom_field_id'],
|
|
543
|
-
ContactTags: ['tag_id'],
|
|
544
|
-
EmailCampaigns: ['campaign_id'],
|
|
545
|
-
EmailCampaignActivities: ['campaign_activity_id'],
|
|
546
|
-
Segments: ['segment_id'],
|
|
547
|
-
Activities: ['activity_id'],
|
|
548
|
-
};
|
|
549
|
-
const candidates = order[objectName] ?? ['id'];
|
|
550
|
-
for (const key of [...candidates, 'id']) {
|
|
551
|
-
const value = body[key];
|
|
552
|
-
if (value !== undefined && value !== null)
|
|
553
|
-
return String(value);
|
|
554
|
-
}
|
|
555
|
-
return '';
|
|
556
|
-
}
|
|
557
|
-
ComputeNewWatermark(records, current) {
|
|
558
|
-
const candidateKeys = ['updated_at', 'edited_at', 'modified_at', 'last_updated_at'];
|
|
559
|
-
let latest;
|
|
560
|
-
for (const record of records) {
|
|
561
|
-
for (const key of candidateKeys) {
|
|
562
|
-
const value = record.Fields[key];
|
|
563
|
-
if (typeof value === 'string' && (!latest || value > latest))
|
|
564
|
-
latest = value;
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
if (!latest)
|
|
568
|
-
return current ?? undefined;
|
|
569
|
-
if (current && latest <= current)
|
|
570
|
-
return current;
|
|
571
|
-
return latest;
|
|
572
|
-
}
|
|
573
|
-
// ── HTTP helpers ────────────────────────────────────────────────
|
|
574
|
-
async ExecuteFetch(url, method, headers, body, timeoutMs) {
|
|
575
|
-
const controller = new AbortController();
|
|
576
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
577
|
-
try {
|
|
578
|
-
return await fetch(url, {
|
|
579
|
-
method,
|
|
580
|
-
headers,
|
|
581
|
-
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
582
|
-
signal: controller.signal,
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
finally {
|
|
586
|
-
clearTimeout(timer);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
async BuildRESTResponse(response) {
|
|
590
|
-
const headers = {};
|
|
591
|
-
response.headers.forEach((v, k) => { headers[k.toLowerCase()] = v; });
|
|
592
|
-
const contentType = headers['content-type'] ?? '';
|
|
593
|
-
let bodyValue;
|
|
594
|
-
if (contentType.includes('application/json')) {
|
|
595
|
-
bodyValue = await this.ParseJsonSafely(response);
|
|
596
|
-
}
|
|
597
|
-
else {
|
|
598
|
-
bodyValue = await response.text();
|
|
599
|
-
}
|
|
600
|
-
return { Status: response.status, Body: bodyValue, Headers: headers };
|
|
601
|
-
}
|
|
602
|
-
async ParseJsonSafely(response) {
|
|
603
|
-
try {
|
|
604
|
-
return await response.json();
|
|
605
|
-
}
|
|
606
|
-
catch {
|
|
607
|
-
return null;
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
ShouldRetry(status) {
|
|
611
|
-
return status === 429 || status === 502 || status === 503 || status === 504;
|
|
612
|
-
}
|
|
613
|
-
ComputeRetryDelay(response, attempt) {
|
|
614
|
-
const retryAfter = response.headers.get('retry-after');
|
|
615
|
-
if (retryAfter) {
|
|
616
|
-
const seconds = Number.parseInt(retryAfter, 10);
|
|
617
|
-
if (!Number.isNaN(seconds) && seconds > 0)
|
|
618
|
-
return seconds * 1000;
|
|
619
|
-
}
|
|
620
|
-
return Math.min(Math.pow(2, attempt) * 1000, 15_000);
|
|
621
|
-
}
|
|
622
|
-
async Throttle(minIntervalMs) {
|
|
623
|
-
const elapsed = Date.now() - this.lastRequestTime;
|
|
624
|
-
if (elapsed < minIntervalMs)
|
|
625
|
-
await this.Sleep(minIntervalMs - elapsed);
|
|
626
|
-
}
|
|
627
|
-
Sleep(ms) {
|
|
628
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
629
|
-
}
|
|
630
|
-
IsAbsoluteURL(value) {
|
|
631
|
-
return /^https?:\/\//i.test(value);
|
|
632
|
-
}
|
|
633
|
-
IsAbsoluteCursorURL(url) {
|
|
634
|
-
return this.IsAbsoluteURL(url) && url.startsWith(CC_BASE_URL);
|
|
635
|
-
}
|
|
636
|
-
BuildCRUDError(response, operation, objectName) {
|
|
637
|
-
const body = response.Body;
|
|
638
|
-
let detail;
|
|
639
|
-
if (typeof body === 'string') {
|
|
640
|
-
detail = body.slice(0, 500);
|
|
641
|
-
}
|
|
642
|
-
else if (body && typeof body === 'object') {
|
|
643
|
-
const asRecord = body;
|
|
644
|
-
const msg = asRecord['error_message'] ?? asRecord['message'] ?? asRecord['error_key'];
|
|
645
|
-
detail = msg !== undefined ? String(msg) : JSON.stringify(body).slice(0, 500);
|
|
646
|
-
}
|
|
647
|
-
else {
|
|
648
|
-
detail = `HTTP ${response.Status}`;
|
|
649
|
-
}
|
|
650
|
-
return {
|
|
651
|
-
Success: false,
|
|
652
|
-
ErrorMessage: `Constant Contact ${operation} failed on ${objectName}: ${detail}`,
|
|
653
|
-
StatusCode: response.Status,
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
};
|
|
657
|
-
ConstantContactConnector = __decorate([
|
|
658
|
-
RegisterClass(BaseIntegrationConnector, 'ConstantContactConnector')
|
|
659
|
-
], ConstantContactConnector);
|
|
660
|
-
export { ConstantContactConnector };
|
|
661
|
-
/** Tree-shaking prevention — import and call from module entry point */
|
|
662
|
-
export function LoadConstantContactConnector() { }
|
|
663
|
-
//# sourceMappingURL=ConstantContactConnector.js.map
|