@memberjunction/integration-connectors 5.35.0 → 5.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/AptifyConnector.d.ts +88 -0
  2. package/dist/AptifyConnector.d.ts.map +1 -0
  3. package/dist/AptifyConnector.js +737 -0
  4. package/dist/AptifyConnector.js.map +1 -0
  5. package/dist/BettyConnector.d.ts +288 -0
  6. package/dist/BettyConnector.d.ts.map +1 -0
  7. package/dist/BettyConnector.js +909 -0
  8. package/dist/BettyConnector.js.map +1 -0
  9. package/dist/BlackbaudConnector.d.ts +64 -0
  10. package/dist/BlackbaudConnector.d.ts.map +1 -0
  11. package/dist/BlackbaudConnector.js +916 -0
  12. package/dist/BlackbaudConnector.js.map +1 -0
  13. package/dist/ConstantContactConnector.d.ts +126 -0
  14. package/dist/ConstantContactConnector.d.ts.map +1 -0
  15. package/dist/ConstantContactConnector.js +636 -0
  16. package/dist/ConstantContactConnector.js.map +1 -0
  17. package/dist/GrowthZoneConnector.d.ts +185 -0
  18. package/dist/GrowthZoneConnector.d.ts.map +1 -0
  19. package/dist/GrowthZoneConnector.js +745 -0
  20. package/dist/GrowthZoneConnector.js.map +1 -0
  21. package/dist/HubSpotConnector.d.ts +2 -2
  22. package/dist/HubSpotConnector.d.ts.map +1 -1
  23. package/dist/HubSpotConnector.js +3 -3
  24. package/dist/HubSpotConnector.js.map +1 -1
  25. package/dist/IMISConnector.d.ts +155 -0
  26. package/dist/IMISConnector.d.ts.map +1 -0
  27. package/dist/IMISConnector.js +686 -0
  28. package/dist/IMISConnector.js.map +1 -0
  29. package/dist/MJToMJConnector.d.ts +279 -0
  30. package/dist/MJToMJConnector.d.ts.map +1 -0
  31. package/dist/MJToMJConnector.js +893 -0
  32. package/dist/MJToMJConnector.js.map +1 -0
  33. package/dist/MagnetMailConnector.d.ts +163 -0
  34. package/dist/MagnetMailConnector.d.ts.map +1 -0
  35. package/dist/MagnetMailConnector.js +816 -0
  36. package/dist/MagnetMailConnector.js.map +1 -0
  37. package/dist/MailchimpConnector.d.ts +188 -0
  38. package/dist/MailchimpConnector.d.ts.map +1 -0
  39. package/dist/MailchimpConnector.js +753 -0
  40. package/dist/MailchimpConnector.js.map +1 -0
  41. package/dist/NetForumConnector.d.ts +51 -0
  42. package/dist/NetForumConnector.d.ts.map +1 -0
  43. package/dist/NetForumConnector.js +419 -0
  44. package/dist/NetForumConnector.js.map +1 -0
  45. package/dist/NetSuiteConnector.d.ts +70 -0
  46. package/dist/NetSuiteConnector.d.ts.map +1 -0
  47. package/dist/NetSuiteConnector.js +605 -0
  48. package/dist/NetSuiteConnector.js.map +1 -0
  49. package/dist/NimbleAMSConnector.d.ts +49 -0
  50. package/dist/NimbleAMSConnector.d.ts.map +1 -0
  51. package/dist/NimbleAMSConnector.js +408 -0
  52. package/dist/NimbleAMSConnector.js.map +1 -0
  53. package/dist/PropFuelConnector.d.ts +68 -0
  54. package/dist/PropFuelConnector.d.ts.map +1 -0
  55. package/dist/PropFuelConnector.js +514 -0
  56. package/dist/PropFuelConnector.js.map +1 -0
  57. package/dist/RasaConnector.d.ts +26 -0
  58. package/dist/RasaConnector.d.ts.map +1 -1
  59. package/dist/RasaConnector.js +233 -1
  60. package/dist/RasaConnector.js.map +1 -1
  61. package/dist/Reach360Connector.d.ts +165 -0
  62. package/dist/Reach360Connector.d.ts.map +1 -0
  63. package/dist/Reach360Connector.js +710 -0
  64. package/dist/Reach360Connector.js.map +1 -0
  65. package/dist/SharePointConnector.d.ts +181 -0
  66. package/dist/SharePointConnector.d.ts.map +1 -0
  67. package/dist/SharePointConnector.js +796 -0
  68. package/dist/SharePointConnector.js.map +1 -0
  69. package/dist/WicketConnector.d.ts +1 -0
  70. package/dist/WicketConnector.d.ts.map +1 -1
  71. package/dist/WicketConnector.js +6 -0
  72. package/dist/WicketConnector.js.map +1 -1
  73. package/dist/WildApricotConnector.d.ts +188 -0
  74. package/dist/WildApricotConnector.d.ts.map +1 -0
  75. package/dist/WildApricotConnector.js +1092 -0
  76. package/dist/WildApricotConnector.js.map +1 -0
  77. package/dist/index.d.ts +16 -0
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +16 -0
  80. package/dist/index.js.map +1 -1
  81. package/package.json +6 -6
@@ -0,0 +1,753 @@
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
+ async DiscoverFields(companyIntegration, objectName, contextUser) {
186
+ const staticFields = await super.DiscoverFields(companyIntegration, objectName, contextUser);
187
+ if (objectName !== 'lists.members')
188
+ return staticFields;
189
+ try {
190
+ const merge = await this.fetchSampleMergeFields(companyIntegration, contextUser);
191
+ const existingNames = new Set(staticFields.map(f => f.Name));
192
+ for (const m of merge) {
193
+ const field = `merge_fields.${m.tag}`;
194
+ if (existingNames.has(field))
195
+ continue;
196
+ staticFields.push({
197
+ Name: field,
198
+ Label: m.name,
199
+ Description: `Mailchimp merge field [${m.type}]${m.help_text ? ` — ${m.help_text}` : ''}`,
200
+ DataType: this.mapMergeTypeToDataType(m.type),
201
+ IsRequired: Boolean(m.required),
202
+ IsUniqueKey: false,
203
+ IsReadOnly: false,
204
+ IsForeignKey: false,
205
+ ForeignKeyTarget: null,
206
+ });
207
+ }
208
+ }
209
+ catch (err) {
210
+ console.warn(`[Mailchimp] Merge-field discovery skipped: ${err instanceof Error ? err.message : String(err)}`);
211
+ }
212
+ return staticFields;
213
+ }
214
+ /**
215
+ * Fetches merge-field definitions from the first active list.
216
+ * Returns an empty array if no lists exist.
217
+ */
218
+ async fetchSampleMergeFields(companyIntegration, contextUser) {
219
+ const auth = await this.Authenticate(companyIntegration, contextUser);
220
+ const base = this.GetBaseURL(companyIntegration, auth);
221
+ const listsURL = `${base}/lists?count=1&fields=lists.id`;
222
+ const listsResponse = await this.MakeHTTPRequest(auth, listsURL, 'GET', this.BuildHeaders(auth));
223
+ const listsBody = listsResponse.Body;
224
+ const listId = listsBody.lists?.[0]?.id;
225
+ if (!listId)
226
+ return [];
227
+ const mergeURL = `${base}/lists/${encodeURIComponent(listId)}/merge-fields?count=${MAX_PAGE_SIZE}`;
228
+ const mergeResponse = await this.MakeHTTPRequest(auth, mergeURL, 'GET', this.BuildHeaders(auth));
229
+ const mergeBody = mergeResponse.Body;
230
+ return mergeBody.merge_fields ?? [];
231
+ }
232
+ /** Maps a Mailchimp merge-field type string to a generic DataType label. */
233
+ mapMergeTypeToDataType(mergeType) {
234
+ switch (mergeType.toLowerCase()) {
235
+ case 'number': return 'decimal';
236
+ case 'date':
237
+ case 'birthday': return 'date';
238
+ case 'address': return 'address';
239
+ case 'phone': return 'phone';
240
+ case 'url':
241
+ case 'imageurl': return 'url';
242
+ case 'zip': return 'string';
243
+ case 'dropdown':
244
+ case 'radio': return 'string';
245
+ default: return 'string';
246
+ }
247
+ }
248
+ // ── CRUD ───────────────────────────────────────────────────────
249
+ /**
250
+ * Creates a new record. For `lists.members`, uses upsert semantics
251
+ * (PUT with subscriber hash) when an `email_address` is supplied.
252
+ */
253
+ async CreateRecord(ctx) {
254
+ const companyIntegration = ctx.CompanyIntegration;
255
+ const contextUser = ctx.ContextUser;
256
+ const auth = await this.Authenticate(companyIntegration, contextUser);
257
+ const base = this.GetBaseURL(companyIntegration, auth);
258
+ const path = this.resolveObjectPath(ctx.ObjectName, ctx.Attributes);
259
+ const method = this.isListMembersUpsert(ctx.ObjectName, ctx.Attributes) ? 'PUT' : 'POST';
260
+ const url = this.buildCrudURL(base, path, method === 'PUT', ctx.Attributes);
261
+ return this.executeCRUD(auth, url, method, ctx.Attributes, ctx.ObjectName, 'CreateRecord');
262
+ }
263
+ /**
264
+ * Updates an existing record via PATCH (partial update).
265
+ */
266
+ async UpdateRecord(ctx) {
267
+ const companyIntegration = ctx.CompanyIntegration;
268
+ const contextUser = ctx.ContextUser;
269
+ const auth = await this.Authenticate(companyIntegration, contextUser);
270
+ const base = this.GetBaseURL(companyIntegration, auth);
271
+ const path = this.resolveObjectPath(ctx.ObjectName, ctx.Attributes);
272
+ const url = `${base}/${path}/${encodeURIComponent(ctx.ExternalID)}`;
273
+ return this.executeCRUD(auth, url, 'PATCH', ctx.Attributes, ctx.ObjectName, 'UpdateRecord');
274
+ }
275
+ /**
276
+ * Deletes (or archives) a record via DELETE.
277
+ */
278
+ async DeleteRecord(ctx) {
279
+ const companyIntegration = ctx.CompanyIntegration;
280
+ const contextUser = ctx.ContextUser;
281
+ const auth = await this.Authenticate(companyIntegration, contextUser);
282
+ const base = this.GetBaseURL(companyIntegration, auth);
283
+ const path = this.resolveObjectPath(ctx.ObjectName, {});
284
+ const url = `${base}/${path}/${encodeURIComponent(ctx.ExternalID)}`;
285
+ return this.executeCRUD(auth, url, 'DELETE', null, ctx.ObjectName, 'DeleteRecord');
286
+ }
287
+ /**
288
+ * Retrieves a single record by ID.
289
+ */
290
+ async GetRecord(ctx) {
291
+ const companyIntegration = ctx.CompanyIntegration;
292
+ const contextUser = ctx.ContextUser;
293
+ const auth = await this.Authenticate(companyIntegration, contextUser);
294
+ const base = this.GetBaseURL(companyIntegration, auth);
295
+ const path = this.resolveObjectPath(ctx.ObjectName, {});
296
+ const url = `${base}/${path}/${encodeURIComponent(ctx.ExternalID)}`;
297
+ const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
298
+ if (response.Status === 404)
299
+ return null;
300
+ if (response.Status < 200 || response.Status >= 300)
301
+ return null;
302
+ const body = response.Body;
303
+ return {
304
+ ExternalID: String(body['id'] ?? ctx.ExternalID),
305
+ ObjectType: ctx.ObjectName,
306
+ Fields: body,
307
+ };
308
+ }
309
+ /** Searches via Mailchimp's `search-members` / `search-campaigns` helpers when available. */
310
+ async SearchRecords(ctx) {
311
+ const companyIntegration = ctx.CompanyIntegration;
312
+ const contextUser = ctx.ContextUser;
313
+ const auth = await this.Authenticate(companyIntegration, contextUser);
314
+ const base = this.GetBaseURL(companyIntegration, auth);
315
+ const pageSize = Math.min(ctx.PageSize ?? DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE);
316
+ const offset = ctx.Page != null && ctx.Page > 1 ? (ctx.Page - 1) * pageSize : 0;
317
+ const queryParts = Object.entries(ctx.Filters).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
318
+ queryParts.push(`count=${pageSize}`, `offset=${offset}`);
319
+ const url = `${base}/${ctx.ObjectName}?${queryParts.join('&')}`;
320
+ const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
321
+ if (response.Status < 200 || response.Status >= 300) {
322
+ return { Records: [], TotalCount: 0, HasMore: false };
323
+ }
324
+ const body = response.Body;
325
+ const records = this.extractCollection(body, ctx.ObjectName);
326
+ return {
327
+ Records: records.map(r => ({
328
+ ExternalID: String(r['id'] ?? ''),
329
+ ObjectType: ctx.ObjectName,
330
+ Fields: r,
331
+ })),
332
+ TotalCount: typeof body.total_items === 'number' ? body.total_items : records.length,
333
+ HasMore: records.length >= pageSize,
334
+ };
335
+ }
336
+ /** Cursor-free paginated listing using offset/count. */
337
+ async ListRecords(ctx) {
338
+ const companyIntegration = ctx.CompanyIntegration;
339
+ const contextUser = ctx.ContextUser;
340
+ const auth = await this.Authenticate(companyIntegration, contextUser);
341
+ const base = this.GetBaseURL(companyIntegration, auth);
342
+ const pageSize = Math.min(ctx.PageSize ?? DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE);
343
+ const cursorOffset = ctx.Cursor ? parseInt(ctx.Cursor, 10) : 0;
344
+ const params = [`count=${pageSize}`, `offset=${cursorOffset}`];
345
+ if (ctx.Filter) {
346
+ for (const [k, v] of Object.entries(ctx.Filter)) {
347
+ params.push(`${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
348
+ }
349
+ }
350
+ const url = `${base}/${ctx.ObjectName}?${params.join('&')}`;
351
+ const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
352
+ if (response.Status < 200 || response.Status >= 300) {
353
+ return { Records: [], HasMore: false };
354
+ }
355
+ const body = response.Body;
356
+ const records = this.extractCollection(body, ctx.ObjectName);
357
+ const hasMore = records.length >= pageSize;
358
+ return {
359
+ Records: records.map(r => ({
360
+ ExternalID: String(r['id'] ?? ''),
361
+ ObjectType: ctx.ObjectName,
362
+ Fields: r,
363
+ })),
364
+ HasMore: hasMore,
365
+ NextCursor: hasMore ? String(cursorOffset + records.length) : undefined,
366
+ };
367
+ }
368
+ // ── BaseRESTIntegrationConnector implementations ─────────────
369
+ async Authenticate(companyIntegration, contextUser) {
370
+ if (this.cachedAuth)
371
+ return this.cachedAuth;
372
+ const config = await this.parseConfig(companyIntegration, contextUser);
373
+ const dc = await this.resolveDataCenter(config);
374
+ const auth = {
375
+ Token: config.ApiKey,
376
+ DataCenter: dc,
377
+ Config: config,
378
+ };
379
+ this.cachedAuth = auth;
380
+ return auth;
381
+ }
382
+ BuildHeaders(auth) {
383
+ const mc = auth;
384
+ // Mailchimp supports HTTP Basic (username=anystring, password=api_key)
385
+ // OR Bearer <access_token> for OAuth2 tokens.
386
+ const isBearer = mc.Config.ApiKey.startsWith('Bearer ');
387
+ if (isBearer) {
388
+ return {
389
+ 'Authorization': mc.Config.ApiKey,
390
+ 'Accept': 'application/json',
391
+ };
392
+ }
393
+ const basic = Buffer.from(`mj:${mc.Config.ApiKey}`).toString('base64');
394
+ return {
395
+ 'Authorization': `Basic ${basic}`,
396
+ 'Accept': 'application/json',
397
+ };
398
+ }
399
+ async MakeHTTPRequest(auth, url, method, headers, body) {
400
+ const mc = auth;
401
+ const maxRetries = mc.Config.MaxRetries ?? DEFAULT_MAX_RETRIES;
402
+ const timeoutMs = mc.Config.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
403
+ const minInterval = mc.Config.MinRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS;
404
+ const finalHeaders = { ...headers };
405
+ if (body !== undefined && !finalHeaders['Content-Type']) {
406
+ finalHeaders['Content-Type'] = 'application/json';
407
+ }
408
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
409
+ await this.throttle(minInterval);
410
+ const response = await this.fetchWithTimeout(url, method, finalHeaders, body, timeoutMs);
411
+ this.lastRequestTime = Date.now();
412
+ if (response.status === 429 || response.status === 503) {
413
+ const delayMs = this.computeBackoff(response, attempt);
414
+ console.warn(`[Mailchimp] ${response.status} ${method} ${url} — retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})`);
415
+ await this.sleep(delayMs);
416
+ continue;
417
+ }
418
+ const parsed = await this.parseResponseBody(response);
419
+ return {
420
+ Status: response.status,
421
+ Body: parsed,
422
+ Headers: this.headersToRecord(response.headers),
423
+ };
424
+ }
425
+ throw new Error(`Mailchimp API request failed after ${maxRetries} retries: ${method} ${url}`);
426
+ }
427
+ NormalizeResponse(rawBody, responseDataKey) {
428
+ if (!rawBody)
429
+ return [];
430
+ // If response is the raw array itself
431
+ if (Array.isArray(rawBody))
432
+ return rawBody;
433
+ if (!responseDataKey)
434
+ return [rawBody];
435
+ const body = rawBody;
436
+ const direct = body[responseDataKey];
437
+ if (Array.isArray(direct))
438
+ return direct;
439
+ // Common alternates for certain endpoints
440
+ for (const alt of ['lists', 'members', 'campaigns', 'orders', 'customers',
441
+ 'products', 'variants', 'templates', 'segments',
442
+ 'emails', 'folders', 'files', 'reports', 'automations',
443
+ 'interest_categories', 'interests', 'webhooks',
444
+ 'merge_fields', 'tags', 'goals', 'notes', 'messages',
445
+ 'conversations', 'queue', 'feedback', 'activity',
446
+ 'carts', 'promo_rules', 'promo_codes', 'results']) {
447
+ const val = body[alt];
448
+ if (Array.isArray(val))
449
+ return val;
450
+ }
451
+ // Single-object response
452
+ return [body];
453
+ }
454
+ ExtractPaginationInfo(rawBody, paginationType, currentPage, currentOffset, pageSize) {
455
+ if (paginationType === 'None')
456
+ return { HasMore: false };
457
+ const body = rawBody;
458
+ const total = typeof body?.total_items === 'number' ? body.total_items : undefined;
459
+ const records = this.NormalizeResponse(rawBody, null);
460
+ const fetched = records.length;
461
+ if (paginationType === 'Offset') {
462
+ const nextOffset = currentOffset + fetched;
463
+ const hasMore = total != null ? nextOffset < total : fetched >= pageSize;
464
+ return {
465
+ HasMore: hasMore,
466
+ NextOffset: hasMore ? nextOffset : undefined,
467
+ TotalRecords: total,
468
+ };
469
+ }
470
+ if (paginationType === 'PageNumber') {
471
+ const hasMore = fetched >= pageSize;
472
+ return {
473
+ HasMore: hasMore,
474
+ NextPage: hasMore ? currentPage + 1 : undefined,
475
+ TotalRecords: total,
476
+ };
477
+ }
478
+ return { HasMore: false };
479
+ }
480
+ BuildPaginatedURL(basePath, obj, _page, offset, _cursor, effectivePageSize) {
481
+ const pageSize = Math.min(effectivePageSize ?? obj.DefaultPageSize ?? DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE);
482
+ const separator = basePath.includes('?') ? '&' : '?';
483
+ return `${basePath}${separator}count=${pageSize}&offset=${offset}`;
484
+ }
485
+ GetBaseURL(_companyIntegration, auth) {
486
+ const mc = auth;
487
+ return `https://${mc.DataCenter}.api.mailchimp.com/3.0`;
488
+ }
489
+ // ─── Helpers ──────────────────────────────────────────────────
490
+ /** Parses credentials from the attached MJ Credential or the Configuration JSON. */
491
+ async parseConfig(companyIntegration, contextUser) {
492
+ const credentialID = companyIntegration.CredentialID;
493
+ if (credentialID) {
494
+ const fromCred = await this.loadFromCredential(credentialID, contextUser);
495
+ if (fromCred)
496
+ return fromCred;
497
+ }
498
+ const configJson = companyIntegration.Configuration;
499
+ if (configJson)
500
+ return this.parseConfigJson(configJson);
501
+ throw new Error('Mailchimp connector: no credentials or Configuration JSON found on CompanyIntegration.');
502
+ }
503
+ /** Loads the credential entity and parses its Values JSON. */
504
+ async loadFromCredential(credentialID, contextUser, provider) {
505
+ const md = provider ?? new Metadata();
506
+ const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
507
+ const loaded = await credential.Load(credentialID);
508
+ if (!loaded || !credential.Values)
509
+ return null;
510
+ try {
511
+ return this.parseConfigJson(credential.Values);
512
+ }
513
+ catch {
514
+ return null;
515
+ }
516
+ }
517
+ /** Parses and validates a Mailchimp config JSON string. */
518
+ parseConfigJson(json) {
519
+ const parsed = JSON.parse(json);
520
+ const apiKey = (parsed['ApiKey'] ?? parsed['apiKey'] ?? parsed['API_Key']);
521
+ if (!apiKey || typeof apiKey !== 'string') {
522
+ throw new Error('Mailchimp config must include a non-empty ApiKey.');
523
+ }
524
+ const parseInt32 = (k) => {
525
+ const raw = parsed[k];
526
+ if (raw == null)
527
+ return undefined;
528
+ const n = Number(raw);
529
+ return Number.isFinite(n) ? Math.floor(n) : undefined;
530
+ };
531
+ return {
532
+ ApiKey: apiKey,
533
+ ServerPrefix: typeof parsed['ServerPrefix'] === 'string'
534
+ ? parsed['ServerPrefix']
535
+ : undefined,
536
+ MaxRetries: parseInt32('MaxRetries'),
537
+ RequestTimeoutMs: parseInt32('RequestTimeoutMs'),
538
+ MinRequestIntervalMs: parseInt32('MinRequestIntervalMs'),
539
+ };
540
+ }
541
+ /**
542
+ * Resolves the data-center prefix (e.g. 'us20') using:
543
+ * 1. Explicit `ServerPrefix` config field
544
+ * 2. Suffix after the last '-' in the API key (e.g. 'abc-us20' → 'us20')
545
+ * 3. OAuth metadata endpoint lookup (fallback for bearer tokens)
546
+ */
547
+ async resolveDataCenter(config) {
548
+ if (config.ServerPrefix)
549
+ return config.ServerPrefix;
550
+ const parsed = this.parseDataCenterFromKey(config.ApiKey);
551
+ if (parsed)
552
+ return parsed;
553
+ return this.fetchDataCenterFromOAuthMetadata(config.ApiKey);
554
+ }
555
+ /** Extracts '<dc>' from a Mailchimp API key of the form '<secret>-<dc>'. */
556
+ parseDataCenterFromKey(apiKey) {
557
+ const idx = apiKey.lastIndexOf('-');
558
+ if (idx < 0 || idx >= apiKey.length - 1)
559
+ return null;
560
+ const suffix = apiKey.slice(idx + 1).toLowerCase();
561
+ if (!/^[a-z]{2}\d{1,3}$/.test(suffix))
562
+ return null;
563
+ return suffix;
564
+ }
565
+ /** Calls `/oauth2/metadata` to discover the data center for a bearer token. */
566
+ async fetchDataCenterFromOAuthMetadata(token) {
567
+ const bearer = token.startsWith('Bearer ') ? token : `OAuth ${token}`;
568
+ const response = await fetch(MAILCHIMP_OAUTH_METADATA_URL, {
569
+ method: 'GET',
570
+ headers: { 'Authorization': bearer, 'Accept': 'application/json' },
571
+ });
572
+ if (!response.ok) {
573
+ throw new Error(`Mailchimp /oauth2/metadata returned ${response.status}; cannot resolve data center. Set ServerPrefix explicitly.`);
574
+ }
575
+ const body = await response.json();
576
+ if (!body.dc) {
577
+ throw new Error('Mailchimp /oauth2/metadata did not include a dc field.');
578
+ }
579
+ return body.dc;
580
+ }
581
+ /**
582
+ * Resolves the object name to an API path, substituting template vars from the
583
+ * provided attribute bag (e.g. `lists.members` → `lists/<list_id>/members`).
584
+ * For objects without template vars, returns the dotted path as a slash path.
585
+ */
586
+ resolveObjectPath(objectName, attributes) {
587
+ // Dotted name like `ecommerce.stores.orders` → slash path
588
+ const pathSegments = objectName.split('.').join('/');
589
+ // Inject template vars from attributes when the segment references a known parent id
590
+ // Convention: the attribute key matches the path template var name (list_id, store_id, campaign_id, etc.)
591
+ return pathSegments.replace(/\{([^}]+)\}/g, (_m, varName) => {
592
+ const val = attributes[varName];
593
+ if (val == null)
594
+ return `{${varName}}`;
595
+ return encodeURIComponent(String(val));
596
+ });
597
+ }
598
+ /** True when creating a list member with an email and the object is `lists.members`. */
599
+ isListMembersUpsert(objectName, attributes) {
600
+ if (objectName !== 'lists.members')
601
+ return false;
602
+ return typeof attributes['email_address'] === 'string' && attributes['email_address'].length > 0;
603
+ }
604
+ /**
605
+ * Builds the CRUD URL. When doing a member upsert, appends the computed
606
+ * subscriber hash as the trailing path segment.
607
+ */
608
+ buildCrudURL(base, path, isUpsertByHash, attributes) {
609
+ if (!isUpsertByHash)
610
+ return `${base}/${path}`;
611
+ const email = String(attributes['email_address']);
612
+ const hash = this.computeSubscriberHash(email);
613
+ return `${base}/${path}/${hash}`;
614
+ }
615
+ /** Mailchimp subscriber hash — MD5 of the lowercased email address. */
616
+ computeSubscriberHash(email) {
617
+ return crypto.createHash('md5').update(email.toLowerCase()).digest('hex');
618
+ }
619
+ /**
620
+ * Executes a CRUD HTTP request and maps the response to a CRUDResult.
621
+ */
622
+ async executeCRUD(auth, url, method, body, objectName, operation) {
623
+ try {
624
+ const response = await this.MakeHTTPRequest(auth, url, method, this.BuildHeaders(auth), body ?? undefined);
625
+ if (response.Status >= 200 && response.Status < 300) {
626
+ const created = response.Body;
627
+ return {
628
+ Success: true,
629
+ ExternalID: String(created?.['id'] ?? ''),
630
+ StatusCode: response.Status,
631
+ };
632
+ }
633
+ return {
634
+ Success: false,
635
+ ErrorMessage: `[Mailchimp] ${operation} ${objectName} failed (HTTP ${response.Status}): ${this.extractErrorMessage(response.Body)}`,
636
+ StatusCode: response.Status,
637
+ };
638
+ }
639
+ catch (err) {
640
+ const message = err instanceof Error ? err.message : String(err);
641
+ return {
642
+ Success: false,
643
+ ErrorMessage: `[Mailchimp] ${operation} ${objectName} failed: ${message}`,
644
+ StatusCode: 500,
645
+ };
646
+ }
647
+ }
648
+ /** Extracts an error message from a Mailchimp error body. */
649
+ extractErrorMessage(body) {
650
+ if (body && typeof body === 'object') {
651
+ const err = body;
652
+ if (err.title || err.detail) {
653
+ return [err.title, err.detail].filter(Boolean).join(' — ');
654
+ }
655
+ }
656
+ return this.previewBody(body);
657
+ }
658
+ /** Best-effort collection extractor for search/list fallbacks. */
659
+ extractCollection(body, objectName) {
660
+ // Try the last path segment of the object name first
661
+ const last = objectName.split('.').pop() ?? objectName;
662
+ const direct = body[last];
663
+ if (Array.isArray(direct))
664
+ return direct;
665
+ const normalized = this.NormalizeResponse(body, null);
666
+ return normalized.filter(r => typeof r === 'object');
667
+ }
668
+ /** Enforces a minimum inter-request interval. */
669
+ async throttle(minIntervalMs) {
670
+ const elapsed = Date.now() - this.lastRequestTime;
671
+ if (elapsed < minIntervalMs) {
672
+ await this.sleep(minIntervalMs - elapsed);
673
+ }
674
+ }
675
+ /** Executes a fetch with an abort-based timeout. */
676
+ async fetchWithTimeout(url, method, headers, body, timeoutMs) {
677
+ const controller = new AbortController();
678
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
679
+ try {
680
+ return await fetch(url, {
681
+ method,
682
+ headers,
683
+ body: body === undefined || body === null
684
+ ? undefined
685
+ : typeof body === 'string' ? body : JSON.stringify(body),
686
+ signal: controller.signal,
687
+ });
688
+ }
689
+ finally {
690
+ clearTimeout(timer);
691
+ }
692
+ }
693
+ /** Parses the response body as JSON; falls back to text. */
694
+ async parseResponseBody(response) {
695
+ const contentType = response.headers.get('content-type') ?? '';
696
+ if (contentType.includes('application/json') || contentType.includes('application/problem+json')) {
697
+ try {
698
+ return await response.json();
699
+ }
700
+ catch {
701
+ return null;
702
+ }
703
+ }
704
+ try {
705
+ return await response.text();
706
+ }
707
+ catch {
708
+ return null;
709
+ }
710
+ }
711
+ /** Converts response Headers to a lowercase-keyed record. */
712
+ headersToRecord(headers) {
713
+ const out = {};
714
+ headers.forEach((v, k) => { out[k.toLowerCase()] = v; });
715
+ return out;
716
+ }
717
+ /** Computes exponential backoff delay, honoring Retry-After when present. */
718
+ computeBackoff(response, attempt) {
719
+ const retryAfter = response.headers.get('retry-after');
720
+ if (retryAfter) {
721
+ const seconds = Number(retryAfter);
722
+ if (Number.isFinite(seconds) && seconds > 0)
723
+ return Math.min(seconds * 1000, 60_000);
724
+ }
725
+ const base = 500 * Math.pow(2, attempt);
726
+ const jitter = Math.floor(Math.random() * 250);
727
+ return Math.min(base + jitter, 30_000);
728
+ }
729
+ /** Sleep helper. */
730
+ sleep(ms) {
731
+ return new Promise(resolve => setTimeout(resolve, ms));
732
+ }
733
+ /** Short preview of a response body for error messages. */
734
+ previewBody(body) {
735
+ if (body == null)
736
+ return '(no body)';
737
+ if (typeof body === 'string')
738
+ return body.slice(0, 400);
739
+ try {
740
+ return JSON.stringify(body).slice(0, 400);
741
+ }
742
+ catch {
743
+ return String(body).slice(0, 400);
744
+ }
745
+ }
746
+ };
747
+ MailchimpConnector = __decorate([
748
+ RegisterClass(BaseIntegrationConnector, 'MailchimpConnector')
749
+ ], MailchimpConnector);
750
+ export { MailchimpConnector };
751
+ /** Tree-shaking prevention — call from module entry point. */
752
+ export function LoadMailchimpConnector() { }
753
+ //# sourceMappingURL=MailchimpConnector.js.map