@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,791 +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
|
-
import * as crypto from 'node:crypto';
|
|
11
|
-
// ─── Constants ────────────────────────────────────────────────────────
|
|
12
|
-
const MAILCHIMP_OAUTH_METADATA_URL = 'https://login.mailchimp.com/oauth2/metadata';
|
|
13
|
-
/** Default page size for list endpoints (Mailchimp max is 1000). */
|
|
14
|
-
const DEFAULT_PAGE_SIZE = 200;
|
|
15
|
-
/** Absolute max records per page enforced by the API. */
|
|
16
|
-
const MAX_PAGE_SIZE = 1000;
|
|
17
|
-
/** Default max retries for 429 / 503 / transient errors. */
|
|
18
|
-
const DEFAULT_MAX_RETRIES = 5;
|
|
19
|
-
/** Default HTTP timeout (ms). */
|
|
20
|
-
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
21
|
-
/** Default throttle between requests (ms). */
|
|
22
|
-
const DEFAULT_MIN_REQUEST_INTERVAL_MS = 100;
|
|
23
|
-
/** Sentinel token replaced at request time with the active watermark ISO value. */
|
|
24
|
-
const WATERMARK_TOKEN = '{watermark}';
|
|
25
|
-
// ─── Connector ───────────────────────────────────────────────────────
|
|
26
|
-
/**
|
|
27
|
-
* Connector for the Mailchimp Marketing API (v3.0).
|
|
28
|
-
*
|
|
29
|
-
* Data-center discovery:
|
|
30
|
-
* - API Key with suffix `-<dc>` (e.g. `abc-us20`) → data center `us20`
|
|
31
|
-
* - Explicit `ServerPrefix` in credentials overrides parsed suffix
|
|
32
|
-
* - OAuth access tokens (bearer) → fetched from `/oauth2/metadata`
|
|
33
|
-
*
|
|
34
|
-
* Authentication:
|
|
35
|
-
* HTTP Basic with username=`anystring`, password=`<api_key>`
|
|
36
|
-
*
|
|
37
|
-
* Features:
|
|
38
|
-
* - Offset/count pagination on all list endpoints
|
|
39
|
-
* - Hierarchical per-parent endpoints via APIPath template variables
|
|
40
|
-
* (e.g. `/lists/{list_id}/members`), driven by IO metadata
|
|
41
|
-
* - Incremental sync via per-endpoint server-side date filters
|
|
42
|
-
* (e.g. `since_last_changed` for members)
|
|
43
|
-
* - Runtime custom field discovery (merge fields per-list)
|
|
44
|
-
* - 429 / 503 exponential backoff; bounded parallelism (10 concurrent, enforced
|
|
45
|
-
* by the engine via BatchMaxRequestCount metadata, this connector itself is
|
|
46
|
-
* single-threaded per `MakeHTTPRequest` call)
|
|
47
|
-
*/
|
|
48
|
-
let MailchimpConnector = class MailchimpConnector extends BaseRESTIntegrationConnector {
|
|
49
|
-
constructor() {
|
|
50
|
-
// ── State ──────────────────────────────────────────────────────
|
|
51
|
-
super(...arguments);
|
|
52
|
-
/** Cached auth context (resolved once per connector instance). */
|
|
53
|
-
this.cachedAuth = null;
|
|
54
|
-
/** Timestamp of the last HTTP request — used for client-side throttling. */
|
|
55
|
-
this.lastRequestTime = 0;
|
|
56
|
-
/** Active watermark for the current FetchChanges call (used to template query params). */
|
|
57
|
-
this.currentWatermark = null;
|
|
58
|
-
}
|
|
59
|
-
// ── Capability getters ────────────────────────────────────────
|
|
60
|
-
get SupportsCreate() { return true; }
|
|
61
|
-
get SupportsUpdate() { return true; }
|
|
62
|
-
get SupportsDelete() { return true; }
|
|
63
|
-
get SupportsSearch() { return true; }
|
|
64
|
-
get SupportsListing() { return true; }
|
|
65
|
-
get IntegrationName() { return 'Mailchimp'; }
|
|
66
|
-
// ── Default configuration ─────────────────────────────────────
|
|
67
|
-
/** Minimal out-of-box sync proposal — audiences + members. */
|
|
68
|
-
GetDefaultConfiguration() {
|
|
69
|
-
return {
|
|
70
|
-
DefaultSchemaName: 'Mailchimp',
|
|
71
|
-
DefaultObjects: [
|
|
72
|
-
{
|
|
73
|
-
SourceObjectName: 'lists',
|
|
74
|
-
TargetTableName: 'MailchimpList',
|
|
75
|
-
TargetEntityName: 'Mailchimp Lists',
|
|
76
|
-
SyncEnabled: true,
|
|
77
|
-
FieldMappings: [],
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
SourceObjectName: 'lists.members',
|
|
81
|
-
TargetTableName: 'MailchimpListMember',
|
|
82
|
-
TargetEntityName: 'Mailchimp List Members',
|
|
83
|
-
SyncEnabled: true,
|
|
84
|
-
FieldMappings: [],
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
SourceObjectName: 'campaigns',
|
|
88
|
-
TargetTableName: 'MailchimpCampaign',
|
|
89
|
-
TargetEntityName: 'Mailchimp Campaigns',
|
|
90
|
-
SyncEnabled: true,
|
|
91
|
-
FieldMappings: [],
|
|
92
|
-
},
|
|
93
|
-
],
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
// ── TestConnection ────────────────────────────────────────────
|
|
97
|
-
/**
|
|
98
|
-
* Pings the Mailchimp API root to verify connectivity and authentication.
|
|
99
|
-
*/
|
|
100
|
-
async TestConnection(companyIntegration, contextUser) {
|
|
101
|
-
try {
|
|
102
|
-
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
103
|
-
const url = `${this.GetBaseURL(companyIntegration, auth)}/ping`;
|
|
104
|
-
const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
|
|
105
|
-
if (response.Status >= 200 && response.Status < 300) {
|
|
106
|
-
const body = response.Body;
|
|
107
|
-
return {
|
|
108
|
-
Success: true,
|
|
109
|
-
Message: `Successfully connected to Mailchimp (${auth.DataCenter}): ${body.health_status ?? 'OK'}`,
|
|
110
|
-
ServerVersion: 'Mailchimp Marketing API v3.0',
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
return {
|
|
114
|
-
Success: false,
|
|
115
|
-
Message: `Mailchimp API returned ${response.Status}: ${this.previewBody(response.Body)}`,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
catch (err) {
|
|
119
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
120
|
-
return { Success: false, Message: `Connection failed: ${message}` };
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
// ── FetchChanges override (watermark templating) ──────────────
|
|
124
|
-
/**
|
|
125
|
-
* Overrides the base implementation to expose the watermark to
|
|
126
|
-
* `AppendDefaultQueryParams`, where the `{watermark}` token is replaced
|
|
127
|
-
* with the active ISO 8601 value.
|
|
128
|
-
*/
|
|
129
|
-
async FetchChanges(ctx) {
|
|
130
|
-
this.currentWatermark = ctx.WatermarkValue;
|
|
131
|
-
try {
|
|
132
|
-
return await super.FetchChanges(ctx);
|
|
133
|
-
}
|
|
134
|
-
finally {
|
|
135
|
-
this.currentWatermark = null;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Overrides base to substitute the active watermark into query-param templates.
|
|
140
|
-
* Any DefaultQueryParam value equal to `{watermark}` is replaced with the current
|
|
141
|
-
* watermark; when there is no watermark, the param is dropped entirely.
|
|
142
|
-
*/
|
|
143
|
-
AppendDefaultQueryParams(url, obj) {
|
|
144
|
-
if (!obj.DefaultQueryParams)
|
|
145
|
-
return super.AppendDefaultQueryParams(url, obj);
|
|
146
|
-
let patched = obj.DefaultQueryParams;
|
|
147
|
-
try {
|
|
148
|
-
const parsed = JSON.parse(obj.DefaultQueryParams);
|
|
149
|
-
const resolved = {};
|
|
150
|
-
for (const [k, v] of Object.entries(parsed)) {
|
|
151
|
-
if (v === WATERMARK_TOKEN) {
|
|
152
|
-
if (this.currentWatermark)
|
|
153
|
-
resolved[k] = this.currentWatermark;
|
|
154
|
-
// else: drop — no watermark → skip this filter param
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
resolved[k] = v;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
patched = JSON.stringify(resolved);
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
// Leave DefaultQueryParams as-is if unparseable — base will warn.
|
|
164
|
-
}
|
|
165
|
-
// Temporarily swap DefaultQueryParams so the base class sees resolved values.
|
|
166
|
-
const originalRaw = obj.DefaultQueryParams;
|
|
167
|
-
try {
|
|
168
|
-
obj.DefaultQueryParams = patched;
|
|
169
|
-
return super.AppendDefaultQueryParams(url, obj);
|
|
170
|
-
}
|
|
171
|
-
finally {
|
|
172
|
-
obj.DefaultQueryParams = originalRaw;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
// ── Runtime custom-field discovery ────────────────────────────
|
|
176
|
-
/**
|
|
177
|
-
* Discovers fields for a Mailchimp object. For `lists.members`, this
|
|
178
|
-
* additionally calls `/lists/{list_id}/merge-fields` on a representative
|
|
179
|
-
* list (the first active list returned by `/lists?count=1`) to surface
|
|
180
|
-
* per-list merge fields as IsCustom fields on top of the static metadata.
|
|
181
|
-
*
|
|
182
|
-
* Merge-field schema varies per audience, so the returned set is a
|
|
183
|
-
* best-effort sample. Production sync should call this per-list.
|
|
184
|
-
*/
|
|
185
|
-
/**
|
|
186
|
-
* Canonical Mailchimp Marketing API v3 top-level objects. Merges the
|
|
187
|
-
* vendor-standard catalog with anything already persisted in
|
|
188
|
-
* IntegrationObject (via super.DiscoverObjects) so curated rows aren't
|
|
189
|
-
* lost. Per-account merge-fields are NOT new tables — they're new
|
|
190
|
-
* columns on `lists.members` and surface via DiscoverFields when
|
|
191
|
-
* IntegrationObject.Name='lists.members'.
|
|
192
|
-
*/
|
|
193
|
-
async DiscoverObjects(companyIntegration, contextUser) {
|
|
194
|
-
const persisted = await super.DiscoverObjects(companyIntegration, contextUser);
|
|
195
|
-
const canonical = [
|
|
196
|
-
{ Name: 'lists', Label: 'Lists / Audiences', Description: 'Audience lists. v3 API /lists.', SupportsIncrementalSync: true, SupportsWrite: true },
|
|
197
|
-
{ Name: 'lists.members', Label: 'List Members', Description: 'Subscribers per list (parent-templated). v3 API /lists/{list_id}/members. Per-list merge fields surface here via DiscoverFields.', SupportsIncrementalSync: true, SupportsWrite: true },
|
|
198
|
-
{ Name: 'lists.merge-fields', Label: 'List Merge Fields', Description: 'Per-audience merge-field schema. v3 API /lists/{list_id}/merge-fields.', SupportsIncrementalSync: false, SupportsWrite: true },
|
|
199
|
-
{ Name: 'lists.segments', Label: 'List Segments', Description: 'Per-audience saved/static segments. v3 API /lists/{list_id}/segments.', SupportsIncrementalSync: true, SupportsWrite: true },
|
|
200
|
-
{ Name: 'lists.interest-categories', Label: 'List Interest Categories', Description: 'Group categories per audience. v3 API /lists/{list_id}/interest-categories.', SupportsIncrementalSync: false, SupportsWrite: true },
|
|
201
|
-
{ Name: 'campaigns', Label: 'Campaigns', Description: 'Email campaigns. v3 API /campaigns.', SupportsIncrementalSync: true, SupportsWrite: true },
|
|
202
|
-
{ Name: 'automations', Label: 'Automations', Description: 'Marketing automations. v3 API /automations.', SupportsIncrementalSync: true, SupportsWrite: false },
|
|
203
|
-
{ Name: 'templates', Label: 'Templates', Description: 'Email templates. v3 API /templates.', SupportsIncrementalSync: false, SupportsWrite: true },
|
|
204
|
-
{ Name: 'reports', Label: 'Reports', Description: 'Per-campaign reporting metrics. v3 API /reports.', SupportsIncrementalSync: true, SupportsWrite: false },
|
|
205
|
-
{ Name: 'conversations', Label: 'Conversations', Description: 'Reply conversations. v3 API /conversations.', SupportsIncrementalSync: true, SupportsWrite: false },
|
|
206
|
-
];
|
|
207
|
-
const byName = new Map();
|
|
208
|
-
for (const o of persisted)
|
|
209
|
-
byName.set(o.Name.toLowerCase(), o);
|
|
210
|
-
for (const c of canonical) {
|
|
211
|
-
if (!byName.has(c.Name.toLowerCase()))
|
|
212
|
-
byName.set(c.Name.toLowerCase(), c);
|
|
213
|
-
}
|
|
214
|
-
return [...byName.values()];
|
|
215
|
-
}
|
|
216
|
-
async DiscoverFields(companyIntegration, objectName, contextUser) {
|
|
217
|
-
const staticFields = await super.DiscoverFields(companyIntegration, objectName, contextUser);
|
|
218
|
-
if (objectName !== 'lists.members')
|
|
219
|
-
return staticFields;
|
|
220
|
-
try {
|
|
221
|
-
const merge = await this.fetchSampleMergeFields(companyIntegration, contextUser);
|
|
222
|
-
const existingNames = new Set(staticFields.map(f => f.Name));
|
|
223
|
-
for (const m of merge) {
|
|
224
|
-
const field = `merge_fields.${m.tag}`;
|
|
225
|
-
if (existingNames.has(field))
|
|
226
|
-
continue;
|
|
227
|
-
staticFields.push({
|
|
228
|
-
Name: field,
|
|
229
|
-
Label: m.name,
|
|
230
|
-
Description: `Mailchimp merge field [${m.type}]${m.help_text ? ` — ${m.help_text}` : ''}`,
|
|
231
|
-
DataType: this.mapMergeTypeToDataType(m.type),
|
|
232
|
-
IsRequired: Boolean(m.required),
|
|
233
|
-
IsUniqueKey: false,
|
|
234
|
-
IsReadOnly: false,
|
|
235
|
-
IsForeignKey: false,
|
|
236
|
-
ForeignKeyTarget: null,
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
catch (err) {
|
|
241
|
-
console.warn(`[Mailchimp] Merge-field discovery skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
242
|
-
}
|
|
243
|
-
return staticFields;
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Fetches merge-field definitions from the first active list.
|
|
247
|
-
* Returns an empty array if no lists exist.
|
|
248
|
-
*/
|
|
249
|
-
async fetchSampleMergeFields(companyIntegration, contextUser) {
|
|
250
|
-
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
251
|
-
const base = this.GetBaseURL(companyIntegration, auth);
|
|
252
|
-
const listsURL = `${base}/lists?count=1&fields=lists.id`;
|
|
253
|
-
const listsResponse = await this.MakeHTTPRequest(auth, listsURL, 'GET', this.BuildHeaders(auth));
|
|
254
|
-
const listsBody = listsResponse.Body;
|
|
255
|
-
const listId = listsBody.lists?.[0]?.id;
|
|
256
|
-
if (!listId)
|
|
257
|
-
return [];
|
|
258
|
-
const mergeURL = `${base}/lists/${encodeURIComponent(listId)}/merge-fields?count=${MAX_PAGE_SIZE}`;
|
|
259
|
-
const mergeResponse = await this.MakeHTTPRequest(auth, mergeURL, 'GET', this.BuildHeaders(auth));
|
|
260
|
-
const mergeBody = mergeResponse.Body;
|
|
261
|
-
return mergeBody.merge_fields ?? [];
|
|
262
|
-
}
|
|
263
|
-
/** Maps a Mailchimp merge-field type string to a generic DataType label. */
|
|
264
|
-
mapMergeTypeToDataType(mergeType) {
|
|
265
|
-
switch (mergeType.toLowerCase()) {
|
|
266
|
-
case 'number': return 'decimal';
|
|
267
|
-
case 'date':
|
|
268
|
-
case 'birthday': return 'date';
|
|
269
|
-
case 'address': return 'address';
|
|
270
|
-
case 'phone': return 'phone';
|
|
271
|
-
case 'url':
|
|
272
|
-
case 'imageurl': return 'url';
|
|
273
|
-
case 'zip': return 'string';
|
|
274
|
-
case 'dropdown':
|
|
275
|
-
case 'radio': return 'string';
|
|
276
|
-
default: return 'string';
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
// ── CRUD ───────────────────────────────────────────────────────
|
|
280
|
-
/**
|
|
281
|
-
* Creates a new record. For `lists.members`, uses upsert semantics
|
|
282
|
-
* (PUT with subscriber hash) when an `email_address` is supplied.
|
|
283
|
-
*/
|
|
284
|
-
async CreateRecord(ctx) {
|
|
285
|
-
const companyIntegration = ctx.CompanyIntegration;
|
|
286
|
-
const contextUser = ctx.ContextUser;
|
|
287
|
-
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
288
|
-
const base = this.GetBaseURL(companyIntegration, auth);
|
|
289
|
-
const path = this.resolveObjectPath(ctx.ObjectName, ctx.Attributes);
|
|
290
|
-
const method = this.isListMembersUpsert(ctx.ObjectName, ctx.Attributes) ? 'PUT' : 'POST';
|
|
291
|
-
const url = this.buildCrudURL(base, path, method === 'PUT', ctx.Attributes);
|
|
292
|
-
return this.executeCRUD(auth, url, method, ctx.Attributes, ctx.ObjectName, 'CreateRecord');
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Updates an existing record via PATCH (partial update).
|
|
296
|
-
*/
|
|
297
|
-
async UpdateRecord(ctx) {
|
|
298
|
-
const companyIntegration = ctx.CompanyIntegration;
|
|
299
|
-
const contextUser = ctx.ContextUser;
|
|
300
|
-
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
301
|
-
const base = this.GetBaseURL(companyIntegration, auth);
|
|
302
|
-
const path = this.resolveObjectPath(ctx.ObjectName, ctx.Attributes);
|
|
303
|
-
const url = `${base}/${path}/${encodeURIComponent(ctx.ExternalID)}`;
|
|
304
|
-
return this.executeCRUD(auth, url, 'PATCH', ctx.Attributes, ctx.ObjectName, 'UpdateRecord');
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Deletes (or archives) a record via DELETE.
|
|
308
|
-
*/
|
|
309
|
-
async DeleteRecord(ctx) {
|
|
310
|
-
const companyIntegration = ctx.CompanyIntegration;
|
|
311
|
-
const contextUser = ctx.ContextUser;
|
|
312
|
-
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
313
|
-
const base = this.GetBaseURL(companyIntegration, auth);
|
|
314
|
-
const path = this.resolveObjectPath(ctx.ObjectName, {});
|
|
315
|
-
const url = `${base}/${path}/${encodeURIComponent(ctx.ExternalID)}`;
|
|
316
|
-
return this.executeCRUD(auth, url, 'DELETE', null, ctx.ObjectName, 'DeleteRecord');
|
|
317
|
-
}
|
|
318
|
-
/**
|
|
319
|
-
* Retrieves a single record by ID.
|
|
320
|
-
*/
|
|
321
|
-
async GetRecord(ctx) {
|
|
322
|
-
const companyIntegration = ctx.CompanyIntegration;
|
|
323
|
-
const contextUser = ctx.ContextUser;
|
|
324
|
-
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
325
|
-
const base = this.GetBaseURL(companyIntegration, auth);
|
|
326
|
-
const path = this.resolveObjectPath(ctx.ObjectName, {});
|
|
327
|
-
const url = `${base}/${path}/${encodeURIComponent(ctx.ExternalID)}`;
|
|
328
|
-
const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
|
|
329
|
-
if (response.Status === 404)
|
|
330
|
-
return null;
|
|
331
|
-
if (response.Status < 200 || response.Status >= 300)
|
|
332
|
-
return null;
|
|
333
|
-
const body = response.Body;
|
|
334
|
-
return {
|
|
335
|
-
ExternalID: String(body['id'] ?? ctx.ExternalID),
|
|
336
|
-
ObjectType: ctx.ObjectName,
|
|
337
|
-
Fields: body,
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
/** Searches via Mailchimp's `search-members` / `search-campaigns` helpers when available. */
|
|
341
|
-
async SearchRecords(ctx) {
|
|
342
|
-
const companyIntegration = ctx.CompanyIntegration;
|
|
343
|
-
const contextUser = ctx.ContextUser;
|
|
344
|
-
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
345
|
-
const base = this.GetBaseURL(companyIntegration, auth);
|
|
346
|
-
const pageSize = Math.min(ctx.PageSize ?? DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE);
|
|
347
|
-
const offset = ctx.Page != null && ctx.Page > 1 ? (ctx.Page - 1) * pageSize : 0;
|
|
348
|
-
const queryParts = Object.entries(ctx.Filters).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
|
|
349
|
-
queryParts.push(`count=${pageSize}`, `offset=${offset}`);
|
|
350
|
-
const url = `${base}/${ctx.ObjectName}?${queryParts.join('&')}`;
|
|
351
|
-
const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
|
|
352
|
-
if (response.Status < 200 || response.Status >= 300) {
|
|
353
|
-
return { Records: [], TotalCount: 0, HasMore: false };
|
|
354
|
-
}
|
|
355
|
-
const body = response.Body;
|
|
356
|
-
const records = this.extractCollection(body, ctx.ObjectName);
|
|
357
|
-
return {
|
|
358
|
-
Records: records.map(r => ({
|
|
359
|
-
ExternalID: String(r['id'] ?? ''),
|
|
360
|
-
ObjectType: ctx.ObjectName,
|
|
361
|
-
Fields: r,
|
|
362
|
-
})),
|
|
363
|
-
TotalCount: typeof body.total_items === 'number' ? body.total_items : records.length,
|
|
364
|
-
HasMore: records.length >= pageSize,
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
/** Cursor-free paginated listing using offset/count. */
|
|
368
|
-
async ListRecords(ctx) {
|
|
369
|
-
const companyIntegration = ctx.CompanyIntegration;
|
|
370
|
-
const contextUser = ctx.ContextUser;
|
|
371
|
-
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
372
|
-
const base = this.GetBaseURL(companyIntegration, auth);
|
|
373
|
-
const pageSize = Math.min(ctx.PageSize ?? DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE);
|
|
374
|
-
const cursorOffset = ctx.Cursor ? parseInt(ctx.Cursor, 10) : 0;
|
|
375
|
-
const params = [`count=${pageSize}`, `offset=${cursorOffset}`];
|
|
376
|
-
if (ctx.Filter) {
|
|
377
|
-
for (const [k, v] of Object.entries(ctx.Filter)) {
|
|
378
|
-
params.push(`${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
const url = `${base}/${ctx.ObjectName}?${params.join('&')}`;
|
|
382
|
-
const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
|
|
383
|
-
if (response.Status < 200 || response.Status >= 300) {
|
|
384
|
-
return { Records: [], HasMore: false };
|
|
385
|
-
}
|
|
386
|
-
const body = response.Body;
|
|
387
|
-
const records = this.extractCollection(body, ctx.ObjectName);
|
|
388
|
-
const hasMore = records.length >= pageSize;
|
|
389
|
-
return {
|
|
390
|
-
Records: records.map(r => ({
|
|
391
|
-
ExternalID: String(r['id'] ?? ''),
|
|
392
|
-
ObjectType: ctx.ObjectName,
|
|
393
|
-
Fields: r,
|
|
394
|
-
})),
|
|
395
|
-
HasMore: hasMore,
|
|
396
|
-
NextCursor: hasMore ? String(cursorOffset + records.length) : undefined,
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
// ── BaseRESTIntegrationConnector implementations ─────────────
|
|
400
|
-
async Authenticate(companyIntegration, contextUser) {
|
|
401
|
-
if (this.cachedAuth)
|
|
402
|
-
return this.cachedAuth;
|
|
403
|
-
const config = await this.parseConfig(companyIntegration, contextUser);
|
|
404
|
-
const dc = await this.resolveDataCenter(config);
|
|
405
|
-
const auth = {
|
|
406
|
-
Token: config.ApiKey,
|
|
407
|
-
DataCenter: dc,
|
|
408
|
-
Config: config,
|
|
409
|
-
};
|
|
410
|
-
this.cachedAuth = auth;
|
|
411
|
-
return auth;
|
|
412
|
-
}
|
|
413
|
-
BuildHeaders(auth) {
|
|
414
|
-
const mc = auth;
|
|
415
|
-
// Mailchimp supports HTTP Basic (username=anystring, password=api_key)
|
|
416
|
-
// OR Bearer <access_token> for OAuth2 tokens.
|
|
417
|
-
const isBearer = mc.Config.ApiKey.startsWith('Bearer ');
|
|
418
|
-
if (isBearer) {
|
|
419
|
-
return {
|
|
420
|
-
'Authorization': mc.Config.ApiKey,
|
|
421
|
-
'Accept': 'application/json',
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
const basic = Buffer.from(`mj:${mc.Config.ApiKey}`).toString('base64');
|
|
425
|
-
return {
|
|
426
|
-
'Authorization': `Basic ${basic}`,
|
|
427
|
-
'Accept': 'application/json',
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
async MakeHTTPRequest(auth, url, method, headers, body) {
|
|
431
|
-
const mc = auth;
|
|
432
|
-
const maxRetries = mc.Config.MaxRetries ?? DEFAULT_MAX_RETRIES;
|
|
433
|
-
const timeoutMs = mc.Config.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
434
|
-
const minInterval = mc.Config.MinRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS;
|
|
435
|
-
const finalHeaders = { ...headers };
|
|
436
|
-
if (body !== undefined && !finalHeaders['Content-Type']) {
|
|
437
|
-
finalHeaders['Content-Type'] = 'application/json';
|
|
438
|
-
}
|
|
439
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
440
|
-
await this.throttle(minInterval);
|
|
441
|
-
const response = await this.fetchWithTimeout(url, method, finalHeaders, body, timeoutMs);
|
|
442
|
-
this.lastRequestTime = Date.now();
|
|
443
|
-
if (response.status === 429 || response.status === 503) {
|
|
444
|
-
const delayMs = this.computeBackoff(response, attempt);
|
|
445
|
-
console.warn(`[Mailchimp] ${response.status} ${method} ${url} — retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})`);
|
|
446
|
-
await this.sleep(delayMs);
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
const parsed = await this.parseResponseBody(response);
|
|
450
|
-
return {
|
|
451
|
-
Status: response.status,
|
|
452
|
-
Body: parsed,
|
|
453
|
-
Headers: this.headersToRecord(response.headers),
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
throw new Error(`Mailchimp API request failed after ${maxRetries} retries: ${method} ${url}`);
|
|
457
|
-
}
|
|
458
|
-
NormalizeResponse(rawBody, responseDataKey) {
|
|
459
|
-
if (!rawBody)
|
|
460
|
-
return [];
|
|
461
|
-
// If response is the raw array itself
|
|
462
|
-
if (Array.isArray(rawBody))
|
|
463
|
-
return rawBody;
|
|
464
|
-
if (!responseDataKey)
|
|
465
|
-
return [rawBody];
|
|
466
|
-
const body = rawBody;
|
|
467
|
-
const direct = body[responseDataKey];
|
|
468
|
-
if (Array.isArray(direct))
|
|
469
|
-
return direct;
|
|
470
|
-
// Common alternates for certain endpoints
|
|
471
|
-
for (const alt of ['lists', 'members', 'campaigns', 'orders', 'customers',
|
|
472
|
-
'products', 'variants', 'templates', 'segments',
|
|
473
|
-
'emails', 'folders', 'files', 'reports', 'automations',
|
|
474
|
-
'interest_categories', 'interests', 'webhooks',
|
|
475
|
-
'merge_fields', 'tags', 'goals', 'notes', 'messages',
|
|
476
|
-
'conversations', 'queue', 'feedback', 'activity',
|
|
477
|
-
'carts', 'promo_rules', 'promo_codes', 'results']) {
|
|
478
|
-
const val = body[alt];
|
|
479
|
-
if (Array.isArray(val))
|
|
480
|
-
return val;
|
|
481
|
-
}
|
|
482
|
-
// Single-object response
|
|
483
|
-
return [body];
|
|
484
|
-
}
|
|
485
|
-
ExtractPaginationInfo(rawBody, paginationType, currentPage, currentOffset, pageSize) {
|
|
486
|
-
if (paginationType === 'None')
|
|
487
|
-
return { HasMore: false };
|
|
488
|
-
const body = rawBody;
|
|
489
|
-
const total = typeof body?.total_items === 'number' ? body.total_items : undefined;
|
|
490
|
-
const records = this.NormalizeResponse(rawBody, null);
|
|
491
|
-
const fetched = records.length;
|
|
492
|
-
if (paginationType === 'Offset') {
|
|
493
|
-
const nextOffset = currentOffset + fetched;
|
|
494
|
-
const hasMore = total != null ? nextOffset < total : fetched >= pageSize;
|
|
495
|
-
return {
|
|
496
|
-
HasMore: hasMore,
|
|
497
|
-
NextOffset: hasMore ? nextOffset : undefined,
|
|
498
|
-
TotalRecords: total,
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
if (paginationType === 'PageNumber') {
|
|
502
|
-
const hasMore = fetched >= pageSize;
|
|
503
|
-
return {
|
|
504
|
-
HasMore: hasMore,
|
|
505
|
-
NextPage: hasMore ? currentPage + 1 : undefined,
|
|
506
|
-
TotalRecords: total,
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
return { HasMore: false };
|
|
510
|
-
}
|
|
511
|
-
BuildPaginatedURL(basePath, obj, _page, offset, _cursor, effectivePageSize) {
|
|
512
|
-
const pageSize = Math.min(effectivePageSize ?? obj.DefaultPageSize ?? DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE);
|
|
513
|
-
const separator = basePath.includes('?') ? '&' : '?';
|
|
514
|
-
return `${basePath}${separator}count=${pageSize}&offset=${offset}`;
|
|
515
|
-
}
|
|
516
|
-
GetBaseURL(_companyIntegration, auth) {
|
|
517
|
-
const mc = auth;
|
|
518
|
-
return `https://${mc.DataCenter}.api.mailchimp.com/3.0`;
|
|
519
|
-
}
|
|
520
|
-
// ─── Helpers ──────────────────────────────────────────────────
|
|
521
|
-
/** Parses credentials from the attached MJ Credential or the Configuration JSON. */
|
|
522
|
-
async parseConfig(companyIntegration, contextUser) {
|
|
523
|
-
const credentialID = companyIntegration.CredentialID;
|
|
524
|
-
if (credentialID) {
|
|
525
|
-
const fromCred = await this.loadFromCredential(credentialID, contextUser);
|
|
526
|
-
if (fromCred)
|
|
527
|
-
return fromCred;
|
|
528
|
-
}
|
|
529
|
-
const configJson = companyIntegration.Configuration;
|
|
530
|
-
if (configJson)
|
|
531
|
-
return this.parseConfigJson(configJson);
|
|
532
|
-
throw new Error('Mailchimp connector: no credentials or Configuration JSON found on CompanyIntegration.');
|
|
533
|
-
}
|
|
534
|
-
/** Loads the credential entity and parses its Values JSON. */
|
|
535
|
-
async loadFromCredential(credentialID, contextUser, provider) {
|
|
536
|
-
const md = provider ?? new Metadata();
|
|
537
|
-
const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
|
|
538
|
-
const loaded = await credential.Load(credentialID);
|
|
539
|
-
if (!loaded || !credential.Values)
|
|
540
|
-
return null;
|
|
541
|
-
try {
|
|
542
|
-
return this.parseConfigJson(credential.Values);
|
|
543
|
-
}
|
|
544
|
-
catch {
|
|
545
|
-
return null;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
/** Parses and validates a Mailchimp config JSON string. */
|
|
549
|
-
parseConfigJson(json) {
|
|
550
|
-
const parsed = JSON.parse(json);
|
|
551
|
-
const apiKey = (parsed['ApiKey'] ?? parsed['apiKey'] ?? parsed['API_Key']);
|
|
552
|
-
if (!apiKey || typeof apiKey !== 'string') {
|
|
553
|
-
throw new Error('Mailchimp config must include a non-empty ApiKey.');
|
|
554
|
-
}
|
|
555
|
-
const parseInt32 = (k) => {
|
|
556
|
-
const raw = parsed[k];
|
|
557
|
-
if (raw == null)
|
|
558
|
-
return undefined;
|
|
559
|
-
const n = Number(raw);
|
|
560
|
-
return Number.isFinite(n) ? Math.floor(n) : undefined;
|
|
561
|
-
};
|
|
562
|
-
return {
|
|
563
|
-
ApiKey: apiKey,
|
|
564
|
-
ServerPrefix: typeof parsed['ServerPrefix'] === 'string'
|
|
565
|
-
? parsed['ServerPrefix']
|
|
566
|
-
: undefined,
|
|
567
|
-
MaxRetries: parseInt32('MaxRetries'),
|
|
568
|
-
RequestTimeoutMs: parseInt32('RequestTimeoutMs'),
|
|
569
|
-
MinRequestIntervalMs: parseInt32('MinRequestIntervalMs'),
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
/**
|
|
573
|
-
* Resolves the data-center prefix (e.g. 'us20') using:
|
|
574
|
-
* 1. Explicit `ServerPrefix` config field
|
|
575
|
-
* 2. Suffix after the last '-' in the API key (e.g. 'abc-us20' → 'us20')
|
|
576
|
-
* 3. OAuth metadata endpoint lookup (fallback for bearer tokens)
|
|
577
|
-
*/
|
|
578
|
-
async resolveDataCenter(config) {
|
|
579
|
-
if (config.ServerPrefix)
|
|
580
|
-
return config.ServerPrefix;
|
|
581
|
-
const parsed = this.parseDataCenterFromKey(config.ApiKey);
|
|
582
|
-
if (parsed)
|
|
583
|
-
return parsed;
|
|
584
|
-
return this.fetchDataCenterFromOAuthMetadata(config.ApiKey);
|
|
585
|
-
}
|
|
586
|
-
/** Extracts '<dc>' from a Mailchimp API key of the form '<secret>-<dc>'. */
|
|
587
|
-
parseDataCenterFromKey(apiKey) {
|
|
588
|
-
const idx = apiKey.lastIndexOf('-');
|
|
589
|
-
if (idx < 0 || idx >= apiKey.length - 1)
|
|
590
|
-
return null;
|
|
591
|
-
const suffix = apiKey.slice(idx + 1).toLowerCase();
|
|
592
|
-
if (!/^[a-z]{2}\d{1,3}$/.test(suffix))
|
|
593
|
-
return null;
|
|
594
|
-
return suffix;
|
|
595
|
-
}
|
|
596
|
-
/** Calls `/oauth2/metadata` to discover the data center for a bearer token. */
|
|
597
|
-
async fetchDataCenterFromOAuthMetadata(token) {
|
|
598
|
-
const bearer = token.startsWith('Bearer ') ? token : `OAuth ${token}`;
|
|
599
|
-
const response = await fetch(MAILCHIMP_OAUTH_METADATA_URL, {
|
|
600
|
-
method: 'GET',
|
|
601
|
-
headers: { 'Authorization': bearer, 'Accept': 'application/json' },
|
|
602
|
-
});
|
|
603
|
-
if (!response.ok) {
|
|
604
|
-
throw new Error(`Mailchimp /oauth2/metadata returned ${response.status}; cannot resolve data center. Set ServerPrefix explicitly.`);
|
|
605
|
-
}
|
|
606
|
-
const body = await response.json();
|
|
607
|
-
if (!body.dc) {
|
|
608
|
-
throw new Error('Mailchimp /oauth2/metadata did not include a dc field.');
|
|
609
|
-
}
|
|
610
|
-
return body.dc;
|
|
611
|
-
}
|
|
612
|
-
/**
|
|
613
|
-
* Resolves the object name to an API path, substituting template vars from the
|
|
614
|
-
* provided attribute bag (e.g. `lists.members` → `lists/<list_id>/members`).
|
|
615
|
-
* For objects without template vars, returns the dotted path as a slash path.
|
|
616
|
-
*/
|
|
617
|
-
resolveObjectPath(objectName, attributes) {
|
|
618
|
-
// Dotted name like `ecommerce.stores.orders` → slash path
|
|
619
|
-
const pathSegments = objectName.split('.').join('/');
|
|
620
|
-
// Inject template vars from attributes when the segment references a known parent id
|
|
621
|
-
// Convention: the attribute key matches the path template var name (list_id, store_id, campaign_id, etc.)
|
|
622
|
-
return pathSegments.replace(/\{([^}]+)\}/g, (_m, varName) => {
|
|
623
|
-
const val = attributes[varName];
|
|
624
|
-
if (val == null)
|
|
625
|
-
return `{${varName}}`;
|
|
626
|
-
return encodeURIComponent(String(val));
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
/** True when creating a list member with an email and the object is `lists.members`. */
|
|
630
|
-
isListMembersUpsert(objectName, attributes) {
|
|
631
|
-
if (objectName !== 'lists.members')
|
|
632
|
-
return false;
|
|
633
|
-
return typeof attributes['email_address'] === 'string' && attributes['email_address'].length > 0;
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* Builds the CRUD URL. When doing a member upsert, appends the computed
|
|
637
|
-
* subscriber hash as the trailing path segment.
|
|
638
|
-
*/
|
|
639
|
-
buildCrudURL(base, path, isUpsertByHash, attributes) {
|
|
640
|
-
if (!isUpsertByHash)
|
|
641
|
-
return `${base}/${path}`;
|
|
642
|
-
const email = String(attributes['email_address']);
|
|
643
|
-
const hash = this.computeSubscriberHash(email);
|
|
644
|
-
return `${base}/${path}/${hash}`;
|
|
645
|
-
}
|
|
646
|
-
/** Mailchimp subscriber hash — MD5 of the lowercased email address. */
|
|
647
|
-
computeSubscriberHash(email) {
|
|
648
|
-
return crypto.createHash('md5').update(email.toLowerCase()).digest('hex');
|
|
649
|
-
}
|
|
650
|
-
/**
|
|
651
|
-
* Executes a CRUD HTTP request and maps the response to a CRUDResult.
|
|
652
|
-
*/
|
|
653
|
-
async executeCRUD(auth, url, method, body, objectName, operation) {
|
|
654
|
-
try {
|
|
655
|
-
const response = await this.MakeHTTPRequest(auth, url, method, this.BuildHeaders(auth), body ?? undefined);
|
|
656
|
-
if (response.Status >= 200 && response.Status < 300) {
|
|
657
|
-
const created = response.Body;
|
|
658
|
-
const externalID = created?.['id'] == null ? undefined : String(created['id']);
|
|
659
|
-
// CREATE-ONLY: a 2xx create with no record ID is a silent record-loss bug (duplicate
|
|
660
|
-
// creates on the next sync). Fail loudly via the base helper. Update/Delete keep their
|
|
661
|
-
// existing semantics — they legitimately may not echo an ID in the body.
|
|
662
|
-
if (operation === 'CreateRecord') {
|
|
663
|
-
return this.BuildCreatedResult(externalID, response.Status, objectName);
|
|
664
|
-
}
|
|
665
|
-
return {
|
|
666
|
-
Success: true,
|
|
667
|
-
ExternalID: externalID ?? '',
|
|
668
|
-
StatusCode: response.Status,
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
return {
|
|
672
|
-
Success: false,
|
|
673
|
-
ErrorMessage: `[Mailchimp] ${operation} ${objectName} failed (HTTP ${response.Status}): ${this.extractErrorMessage(response.Body)}`,
|
|
674
|
-
StatusCode: response.Status,
|
|
675
|
-
};
|
|
676
|
-
}
|
|
677
|
-
catch (err) {
|
|
678
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
679
|
-
return {
|
|
680
|
-
Success: false,
|
|
681
|
-
ErrorMessage: `[Mailchimp] ${operation} ${objectName} failed: ${message}`,
|
|
682
|
-
StatusCode: 500,
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
/** Extracts an error message from a Mailchimp error body. */
|
|
687
|
-
extractErrorMessage(body) {
|
|
688
|
-
if (body && typeof body === 'object') {
|
|
689
|
-
const err = body;
|
|
690
|
-
if (err.title || err.detail) {
|
|
691
|
-
return [err.title, err.detail].filter(Boolean).join(' — ');
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
return this.previewBody(body);
|
|
695
|
-
}
|
|
696
|
-
/** Best-effort collection extractor for search/list fallbacks. */
|
|
697
|
-
extractCollection(body, objectName) {
|
|
698
|
-
// Try the last path segment of the object name first
|
|
699
|
-
const last = objectName.split('.').pop() ?? objectName;
|
|
700
|
-
const direct = body[last];
|
|
701
|
-
if (Array.isArray(direct))
|
|
702
|
-
return direct;
|
|
703
|
-
const normalized = this.NormalizeResponse(body, null);
|
|
704
|
-
return normalized.filter(r => typeof r === 'object');
|
|
705
|
-
}
|
|
706
|
-
/** Enforces a minimum inter-request interval. */
|
|
707
|
-
async throttle(minIntervalMs) {
|
|
708
|
-
const elapsed = Date.now() - this.lastRequestTime;
|
|
709
|
-
if (elapsed < minIntervalMs) {
|
|
710
|
-
await this.sleep(minIntervalMs - elapsed);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
/** Executes a fetch with an abort-based timeout. */
|
|
714
|
-
async fetchWithTimeout(url, method, headers, body, timeoutMs) {
|
|
715
|
-
const controller = new AbortController();
|
|
716
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
717
|
-
try {
|
|
718
|
-
return await fetch(url, {
|
|
719
|
-
method,
|
|
720
|
-
headers,
|
|
721
|
-
body: body === undefined || body === null
|
|
722
|
-
? undefined
|
|
723
|
-
: typeof body === 'string' ? body : JSON.stringify(body),
|
|
724
|
-
signal: controller.signal,
|
|
725
|
-
});
|
|
726
|
-
}
|
|
727
|
-
finally {
|
|
728
|
-
clearTimeout(timer);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
/** Parses the response body as JSON; falls back to text. */
|
|
732
|
-
async parseResponseBody(response) {
|
|
733
|
-
const contentType = response.headers.get('content-type') ?? '';
|
|
734
|
-
if (contentType.includes('application/json') || contentType.includes('application/problem+json')) {
|
|
735
|
-
try {
|
|
736
|
-
return await response.json();
|
|
737
|
-
}
|
|
738
|
-
catch {
|
|
739
|
-
return null;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
try {
|
|
743
|
-
return await response.text();
|
|
744
|
-
}
|
|
745
|
-
catch {
|
|
746
|
-
return null;
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
/** Converts response Headers to a lowercase-keyed record. */
|
|
750
|
-
headersToRecord(headers) {
|
|
751
|
-
const out = {};
|
|
752
|
-
headers.forEach((v, k) => { out[k.toLowerCase()] = v; });
|
|
753
|
-
return out;
|
|
754
|
-
}
|
|
755
|
-
/** Computes exponential backoff delay, honoring Retry-After when present. */
|
|
756
|
-
computeBackoff(response, attempt) {
|
|
757
|
-
const retryAfter = response.headers.get('retry-after');
|
|
758
|
-
if (retryAfter) {
|
|
759
|
-
const seconds = Number(retryAfter);
|
|
760
|
-
if (Number.isFinite(seconds) && seconds > 0)
|
|
761
|
-
return Math.min(seconds * 1000, 60_000);
|
|
762
|
-
}
|
|
763
|
-
const base = 500 * Math.pow(2, attempt);
|
|
764
|
-
const jitter = Math.floor(Math.random() * 250);
|
|
765
|
-
return Math.min(base + jitter, 30_000);
|
|
766
|
-
}
|
|
767
|
-
/** Sleep helper. */
|
|
768
|
-
sleep(ms) {
|
|
769
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
770
|
-
}
|
|
771
|
-
/** Short preview of a response body for error messages. */
|
|
772
|
-
previewBody(body) {
|
|
773
|
-
if (body == null)
|
|
774
|
-
return '(no body)';
|
|
775
|
-
if (typeof body === 'string')
|
|
776
|
-
return body.slice(0, 400);
|
|
777
|
-
try {
|
|
778
|
-
return JSON.stringify(body).slice(0, 400);
|
|
779
|
-
}
|
|
780
|
-
catch {
|
|
781
|
-
return String(body).slice(0, 400);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
};
|
|
785
|
-
MailchimpConnector = __decorate([
|
|
786
|
-
RegisterClass(BaseIntegrationConnector, 'MailchimpConnector')
|
|
787
|
-
], MailchimpConnector);
|
|
788
|
-
export { MailchimpConnector };
|
|
789
|
-
/** Tree-shaking prevention — call from module entry point. */
|
|
790
|
-
export function LoadMailchimpConnector() { }
|
|
791
|
-
//# sourceMappingURL=MailchimpConnector.js.map
|