@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.
Files changed (155) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +109 -277
  3. package/dist/index.d.ts +0 -36
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +7 -38
  6. package/dist/index.js.map +1 -1
  7. package/package.json +14 -19
  8. package/dist/AptifyConnector.d.ts +0 -95
  9. package/dist/AptifyConnector.d.ts.map +0 -1
  10. package/dist/AptifyConnector.js +0 -760
  11. package/dist/AptifyConnector.js.map +0 -1
  12. package/dist/BlackbaudConnector.d.ts +0 -64
  13. package/dist/BlackbaudConnector.d.ts.map +0 -1
  14. package/dist/BlackbaudConnector.js +0 -916
  15. package/dist/BlackbaudConnector.js.map +0 -1
  16. package/dist/ConstantContactConnector.d.ts +0 -137
  17. package/dist/ConstantContactConnector.d.ts.map +0 -1
  18. package/dist/ConstantContactConnector.js +0 -663
  19. package/dist/ConstantContactConnector.js.map +0 -1
  20. package/dist/CventConnector.d.ts +0 -146
  21. package/dist/CventConnector.d.ts.map +0 -1
  22. package/dist/CventConnector.js +0 -540
  23. package/dist/CventConnector.js.map +0 -1
  24. package/dist/DynamicsDataverseConnector.d.ts +0 -296
  25. package/dist/DynamicsDataverseConnector.d.ts.map +0 -1
  26. package/dist/DynamicsDataverseConnector.js +0 -790
  27. package/dist/DynamicsDataverseConnector.js.map +0 -1
  28. package/dist/FileFeedConnector.d.ts +0 -64
  29. package/dist/FileFeedConnector.d.ts.map +0 -1
  30. package/dist/FileFeedConnector.js +0 -190
  31. package/dist/FileFeedConnector.js.map +0 -1
  32. package/dist/FontevaConnector.d.ts +0 -153
  33. package/dist/FontevaConnector.d.ts.map +0 -1
  34. package/dist/FontevaConnector.js +0 -421
  35. package/dist/FontevaConnector.js.map +0 -1
  36. package/dist/GrowthZoneConnector.d.ts +0 -183
  37. package/dist/GrowthZoneConnector.d.ts.map +0 -1
  38. package/dist/GrowthZoneConnector.js +0 -702
  39. package/dist/GrowthZoneConnector.js.map +0 -1
  40. package/dist/HivebriteConnector.d.ts +0 -151
  41. package/dist/HivebriteConnector.d.ts.map +0 -1
  42. package/dist/HivebriteConnector.js +0 -533
  43. package/dist/HivebriteConnector.js.map +0 -1
  44. package/dist/HubSpotConnector.d.ts +0 -523
  45. package/dist/HubSpotConnector.d.ts.map +0 -1
  46. package/dist/HubSpotConnector.js +0 -3110
  47. package/dist/HubSpotConnector.js.map +0 -1
  48. package/dist/IMISConnector.d.ts +0 -171
  49. package/dist/IMISConnector.d.ts.map +0 -1
  50. package/dist/IMISConnector.js +0 -695
  51. package/dist/IMISConnector.js.map +0 -1
  52. package/dist/MJToMJConnector.d.ts +0 -279
  53. package/dist/MJToMJConnector.d.ts.map +0 -1
  54. package/dist/MJToMJConnector.js +0 -893
  55. package/dist/MJToMJConnector.js.map +0 -1
  56. package/dist/MagnetMailConnector.d.ts +0 -163
  57. package/dist/MagnetMailConnector.d.ts.map +0 -1
  58. package/dist/MagnetMailConnector.js +0 -823
  59. package/dist/MagnetMailConnector.js.map +0 -1
  60. package/dist/MailchimpConnector.d.ts +0 -197
  61. package/dist/MailchimpConnector.d.ts.map +0 -1
  62. package/dist/MailchimpConnector.js +0 -791
  63. package/dist/MailchimpConnector.js.map +0 -1
  64. package/dist/MemberSuiteConnector.d.ts +0 -185
  65. package/dist/MemberSuiteConnector.d.ts.map +0 -1
  66. package/dist/MemberSuiteConnector.js +0 -762
  67. package/dist/MemberSuiteConnector.js.map +0 -1
  68. package/dist/NeonCRMConnector.d.ts +0 -273
  69. package/dist/NeonCRMConnector.d.ts.map +0 -1
  70. package/dist/NeonCRMConnector.js +0 -1039
  71. package/dist/NeonCRMConnector.js.map +0 -1
  72. package/dist/NetForumConnector.d.ts +0 -170
  73. package/dist/NetForumConnector.d.ts.map +0 -1
  74. package/dist/NetForumConnector.js +0 -778
  75. package/dist/NetForumConnector.js.map +0 -1
  76. package/dist/NetSuiteConnector.d.ts +0 -136
  77. package/dist/NetSuiteConnector.d.ts.map +0 -1
  78. package/dist/NetSuiteConnector.js +0 -703
  79. package/dist/NetSuiteConnector.js.map +0 -1
  80. package/dist/NimbleAMSConnector.d.ts +0 -142
  81. package/dist/NimbleAMSConnector.d.ts.map +0 -1
  82. package/dist/NimbleAMSConnector.js +0 -703
  83. package/dist/NimbleAMSConnector.js.map +0 -1
  84. package/dist/NoviConnector.d.ts +0 -178
  85. package/dist/NoviConnector.d.ts.map +0 -1
  86. package/dist/NoviConnector.js +0 -566
  87. package/dist/NoviConnector.js.map +0 -1
  88. package/dist/ORCIDConnector.d.ts +0 -207
  89. package/dist/ORCIDConnector.d.ts.map +0 -1
  90. package/dist/ORCIDConnector.js +0 -708
  91. package/dist/ORCIDConnector.js.map +0 -1
  92. package/dist/OpenWaterConnector.d.ts +0 -173
  93. package/dist/OpenWaterConnector.d.ts.map +0 -1
  94. package/dist/OpenWaterConnector.js +0 -893
  95. package/dist/OpenWaterConnector.js.map +0 -1
  96. package/dist/PathLMSConnector.d.ts +0 -395
  97. package/dist/PathLMSConnector.d.ts.map +0 -1
  98. package/dist/PathLMSConnector.js +0 -1281
  99. package/dist/PathLMSConnector.js.map +0 -1
  100. package/dist/PheedLoopConnector.d.ts +0 -119
  101. package/dist/PheedLoopConnector.d.ts.map +0 -1
  102. package/dist/PheedLoopConnector.js +0 -429
  103. package/dist/PheedLoopConnector.js.map +0 -1
  104. package/dist/PropFuelConnector.d.ts +0 -153
  105. package/dist/PropFuelConnector.d.ts.map +0 -1
  106. package/dist/PropFuelConnector.js +0 -450
  107. package/dist/PropFuelConnector.js.map +0 -1
  108. package/dist/QuickBooksConnector.d.ts +0 -92
  109. package/dist/QuickBooksConnector.d.ts.map +0 -1
  110. package/dist/QuickBooksConnector.js +0 -713
  111. package/dist/QuickBooksConnector.js.map +0 -1
  112. package/dist/RasaConnector.d.ts +0 -152
  113. package/dist/RasaConnector.d.ts.map +0 -1
  114. package/dist/RasaConnector.js +0 -929
  115. package/dist/RasaConnector.js.map +0 -1
  116. package/dist/Reach360Connector.d.ts +0 -175
  117. package/dist/Reach360Connector.d.ts.map +0 -1
  118. package/dist/Reach360Connector.js +0 -796
  119. package/dist/Reach360Connector.js.map +0 -1
  120. package/dist/RelationalDBConnector.d.ts +0 -108
  121. package/dist/RelationalDBConnector.d.ts.map +0 -1
  122. package/dist/RelationalDBConnector.js +0 -228
  123. package/dist/RelationalDBConnector.js.map +0 -1
  124. package/dist/RhythmConnector.d.ts +0 -188
  125. package/dist/RhythmConnector.d.ts.map +0 -1
  126. package/dist/RhythmConnector.js +0 -784
  127. package/dist/RhythmConnector.js.map +0 -1
  128. package/dist/SageIntacctConnector.d.ts +0 -337
  129. package/dist/SageIntacctConnector.d.ts.map +0 -1
  130. package/dist/SageIntacctConnector.js +0 -2081
  131. package/dist/SageIntacctConnector.js.map +0 -1
  132. package/dist/SalesforceConnector.d.ts +0 -461
  133. package/dist/SalesforceConnector.d.ts.map +0 -1
  134. package/dist/SalesforceConnector.js +0 -1862
  135. package/dist/SalesforceConnector.js.map +0 -1
  136. package/dist/SharePointConnector.d.ts +0 -188
  137. package/dist/SharePointConnector.d.ts.map +0 -1
  138. package/dist/SharePointConnector.js +0 -716
  139. package/dist/SharePointConnector.js.map +0 -1
  140. package/dist/WicketConnector.d.ts +0 -252
  141. package/dist/WicketConnector.d.ts.map +0 -1
  142. package/dist/WicketConnector.js +0 -938
  143. package/dist/WicketConnector.js.map +0 -1
  144. package/dist/WildApricotConnector.d.ts +0 -201
  145. package/dist/WildApricotConnector.d.ts.map +0 -1
  146. package/dist/WildApricotConnector.js +0 -1129
  147. package/dist/WildApricotConnector.js.map +0 -1
  148. package/dist/YourMembershipConnector.d.ts +0 -349
  149. package/dist/YourMembershipConnector.d.ts.map +0 -1
  150. package/dist/YourMembershipConnector.js +0 -4062
  151. package/dist/YourMembershipConnector.js.map +0 -1
  152. package/dist/generate-integration-actions.d.ts +0 -23
  153. package/dist/generate-integration-actions.d.ts.map +0 -1
  154. package/dist/generate-integration-actions.js +0 -201
  155. package/dist/generate-integration-actions.js.map +0 -1
@@ -1,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