@memberjunction/integration-connectors 5.34.1 → 5.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AptifyConnector.d.ts +88 -0
- package/dist/AptifyConnector.d.ts.map +1 -0
- package/dist/AptifyConnector.js +737 -0
- package/dist/AptifyConnector.js.map +1 -0
- package/dist/BettyConnector.d.ts +288 -0
- package/dist/BettyConnector.d.ts.map +1 -0
- package/dist/BettyConnector.js +909 -0
- package/dist/BettyConnector.js.map +1 -0
- package/dist/BlackbaudConnector.d.ts +64 -0
- package/dist/BlackbaudConnector.d.ts.map +1 -0
- package/dist/BlackbaudConnector.js +916 -0
- package/dist/BlackbaudConnector.js.map +1 -0
- package/dist/ConstantContactConnector.d.ts +126 -0
- package/dist/ConstantContactConnector.d.ts.map +1 -0
- package/dist/ConstantContactConnector.js +636 -0
- package/dist/ConstantContactConnector.js.map +1 -0
- package/dist/GrowthZoneConnector.d.ts +185 -0
- package/dist/GrowthZoneConnector.d.ts.map +1 -0
- package/dist/GrowthZoneConnector.js +745 -0
- package/dist/GrowthZoneConnector.js.map +1 -0
- package/dist/HubSpotConnector.d.ts +2 -2
- package/dist/HubSpotConnector.d.ts.map +1 -1
- package/dist/HubSpotConnector.js +3 -3
- package/dist/HubSpotConnector.js.map +1 -1
- package/dist/IMISConnector.d.ts +155 -0
- package/dist/IMISConnector.d.ts.map +1 -0
- package/dist/IMISConnector.js +686 -0
- package/dist/IMISConnector.js.map +1 -0
- package/dist/MJToMJConnector.d.ts +279 -0
- package/dist/MJToMJConnector.d.ts.map +1 -0
- package/dist/MJToMJConnector.js +893 -0
- package/dist/MJToMJConnector.js.map +1 -0
- package/dist/MagnetMailConnector.d.ts +163 -0
- package/dist/MagnetMailConnector.d.ts.map +1 -0
- package/dist/MagnetMailConnector.js +816 -0
- package/dist/MagnetMailConnector.js.map +1 -0
- package/dist/MailchimpConnector.d.ts +188 -0
- package/dist/MailchimpConnector.d.ts.map +1 -0
- package/dist/MailchimpConnector.js +753 -0
- package/dist/MailchimpConnector.js.map +1 -0
- package/dist/NetForumConnector.d.ts +51 -0
- package/dist/NetForumConnector.d.ts.map +1 -0
- package/dist/NetForumConnector.js +419 -0
- package/dist/NetForumConnector.js.map +1 -0
- package/dist/NetSuiteConnector.d.ts +70 -0
- package/dist/NetSuiteConnector.d.ts.map +1 -0
- package/dist/NetSuiteConnector.js +605 -0
- package/dist/NetSuiteConnector.js.map +1 -0
- package/dist/NimbleAMSConnector.d.ts +49 -0
- package/dist/NimbleAMSConnector.d.ts.map +1 -0
- package/dist/NimbleAMSConnector.js +408 -0
- package/dist/NimbleAMSConnector.js.map +1 -0
- package/dist/PropFuelConnector.d.ts +68 -0
- package/dist/PropFuelConnector.d.ts.map +1 -0
- package/dist/PropFuelConnector.js +514 -0
- package/dist/PropFuelConnector.js.map +1 -0
- package/dist/RasaConnector.d.ts +26 -0
- package/dist/RasaConnector.d.ts.map +1 -1
- package/dist/RasaConnector.js +233 -1
- package/dist/RasaConnector.js.map +1 -1
- package/dist/Reach360Connector.d.ts +165 -0
- package/dist/Reach360Connector.d.ts.map +1 -0
- package/dist/Reach360Connector.js +710 -0
- package/dist/Reach360Connector.js.map +1 -0
- package/dist/SharePointConnector.d.ts +181 -0
- package/dist/SharePointConnector.d.ts.map +1 -0
- package/dist/SharePointConnector.js +796 -0
- package/dist/SharePointConnector.js.map +1 -0
- package/dist/WicketConnector.d.ts +1 -0
- package/dist/WicketConnector.d.ts.map +1 -1
- package/dist/WicketConnector.js +6 -0
- package/dist/WicketConnector.js.map +1 -1
- package/dist/WildApricotConnector.d.ts +188 -0
- package/dist/WildApricotConnector.d.ts.map +1 -0
- package/dist/WildApricotConnector.js +1092 -0
- package/dist/WildApricotConnector.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,816 @@
|
|
|
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
|
+
/**
|
|
8
|
+
* ⚠️ NEEDS T10 LIVE VERIFICATION BEFORE PRODUCTION ⚠️
|
|
9
|
+
*
|
|
10
|
+
* 2026-05-20 vendor-truth audit (vs public WSDL at hlma-apie1.magnetmail.net/mmapi.asmx?WSDL)
|
|
11
|
+
* found 8 wire-level mismatches between this connector and the vendor's actual API
|
|
12
|
+
* surface. **All 8 are now fixed** based on WSDL evidence. T10 sandbox verification
|
|
13
|
+
* is still needed before customer rollout.
|
|
14
|
+
*
|
|
15
|
+
* Fixed in this round:
|
|
16
|
+
* ✓ SOAP namespace: changed 'http://api.magnetmail.net/' → 'http://www.magnetmail.net/'
|
|
17
|
+
* ✓ Auth placement: moved from operation body to <mmAuthHeader> SOAP HEADER
|
|
18
|
+
* ✓ Authenticate field: 'user_id' → 'username'
|
|
19
|
+
* ✓ Authenticate response: now reads <sessionId> (with capital S/I) as primary
|
|
20
|
+
* ✓ Pagination: 'start_row'/'row_count' → 'pageNumber'/'pageCount' with
|
|
21
|
+
* SupportsPagination=false on operations that take no pagination
|
|
22
|
+
* (searchForRecipients, getMessagesUTC)
|
|
23
|
+
* ✓ getMessagesUTC watermark params in metadata: 'send_date_*' →
|
|
24
|
+
* 'sentStartDate'/'sentEndDate'
|
|
25
|
+
* ✓ addRecipient Groups[] complex array: declared via Recipients DQP hint
|
|
26
|
+
* ✓ searchForRecipients <criteria> wrapper: envelope-builder branches via the
|
|
27
|
+
* COMPLEX_WRAPPERS map so the filter args nest inside <criteria> for this
|
|
28
|
+
* specific operation. Add entries to that map for any future operation the
|
|
29
|
+
* WSDL wraps similarly.
|
|
30
|
+
*
|
|
31
|
+
* Until T10 (live tenant) confirms this connector now exchanges successful calls,
|
|
32
|
+
* keep customer-facing rollout gated.
|
|
33
|
+
*/
|
|
34
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
35
|
+
import { Metadata } from '@memberjunction/core';
|
|
36
|
+
import { IntegrationEngineBase } from '@memberjunction/integration-engine-base';
|
|
37
|
+
import { BaseIntegrationConnector, } from '@memberjunction/integration-engine';
|
|
38
|
+
// ─── Constants ───────────────────────────────────────────────────────
|
|
39
|
+
/** Default MagnetMail SOAP endpoint (shared hostname, not per-customer). */
|
|
40
|
+
const DEFAULT_ENDPOINT = 'https://hlma-apie1.magnetmail.net/mmapi.asmx';
|
|
41
|
+
/** Default MagnetMail SOAP namespace (derived from the public WSDL's tns). */
|
|
42
|
+
const DEFAULT_NAMESPACE = 'http://www.magnetmail.net/';
|
|
43
|
+
/** Default request timeout for MagnetMail (slower than typical REST). */
|
|
44
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 60000;
|
|
45
|
+
/** Default minimum milliseconds between API calls. */
|
|
46
|
+
const DEFAULT_MIN_REQUEST_INTERVAL_MS = 500;
|
|
47
|
+
/** Default retry count. */
|
|
48
|
+
const DEFAULT_MAX_RETRIES = 4;
|
|
49
|
+
/** Default session TTL — re-authenticate after 30 minutes of use. */
|
|
50
|
+
const DEFAULT_SESSION_TTL_MS = 30 * 60 * 1000;
|
|
51
|
+
/** Keys in DefaultQueryParams JSON that are reserved for connector behavior, not SOAP args. */
|
|
52
|
+
const RESERVED_META_KEYS = new Set([
|
|
53
|
+
'create_action', 'update_action', 'delete_action', 'detail_action',
|
|
54
|
+
'count_action', 'run_action', 'enhanced_action',
|
|
55
|
+
'beta_create_action', 'beta_status_action',
|
|
56
|
+
'watermark_field', 'watermark_from_param', 'watermark_to_param',
|
|
57
|
+
]);
|
|
58
|
+
// ─── MagnetMail Static Object Metadata for Action Generation ─────────
|
|
59
|
+
const MAGNETMAIL_OBJECTS = [
|
|
60
|
+
{
|
|
61
|
+
Name: 'Recipients',
|
|
62
|
+
DisplayName: 'Recipient',
|
|
63
|
+
Description: 'An email recipient / contact record in MagnetMail.',
|
|
64
|
+
SupportsWrite: true,
|
|
65
|
+
Fields: [
|
|
66
|
+
{ Name: 'recipient_id', DisplayName: 'Recipient ID', Type: 'number', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'MagnetMail recipient identifier (integer).' },
|
|
67
|
+
{ Name: 'email', DisplayName: 'Email', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Primary email address (de-facto natural key).' },
|
|
68
|
+
{ Name: 'first_name', DisplayName: 'First Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'First name.' },
|
|
69
|
+
{ Name: 'last_name', DisplayName: 'Last Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Last name.' },
|
|
70
|
+
{ Name: 'company', DisplayName: 'Company', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Company name.' },
|
|
71
|
+
{ Name: 'phone', DisplayName: 'Phone', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Phone number.' },
|
|
72
|
+
{ Name: 'unsubscribed', DisplayName: 'Unsubscribed', Type: 'boolean', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'True when the recipient has unsubscribed.' },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
Name: 'Groups',
|
|
77
|
+
DisplayName: 'Group',
|
|
78
|
+
Description: 'A distribution group in MagnetMail.',
|
|
79
|
+
SupportsWrite: true,
|
|
80
|
+
Fields: [
|
|
81
|
+
{ Name: 'group_id', DisplayName: 'Group ID', Type: 'number', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Group identifier.' },
|
|
82
|
+
{ Name: 'group_name', DisplayName: 'Group Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Group name.' },
|
|
83
|
+
{ Name: 'description', DisplayName: 'Description', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Group description.' },
|
|
84
|
+
{ Name: 'category_id', DisplayName: 'Category ID', Type: 'number', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Associated category.' },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
Name: 'Messages',
|
|
89
|
+
DisplayName: 'Message',
|
|
90
|
+
Description: 'An email message / template in MagnetMail.',
|
|
91
|
+
SupportsWrite: true,
|
|
92
|
+
Fields: [
|
|
93
|
+
{ Name: 'message_id', DisplayName: 'Message ID', Type: 'number', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Message identifier.' },
|
|
94
|
+
{ Name: 'message_name', DisplayName: 'Message Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Message name.' },
|
|
95
|
+
{ Name: 'subject', DisplayName: 'Subject', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Email subject.' },
|
|
96
|
+
{ Name: 'from_email', DisplayName: 'From Email', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Sender email address.' },
|
|
97
|
+
{ Name: 'createDate', DisplayName: 'Create Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Message creation timestamp.' },
|
|
98
|
+
{ Name: 'lastSent', DisplayName: 'Last Sent', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Most recent send timestamp.' },
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
Name: 'UploadJobs',
|
|
103
|
+
DisplayName: 'Upload Job',
|
|
104
|
+
Description: 'An async bulk CSV upload job in MagnetMail.',
|
|
105
|
+
SupportsWrite: true,
|
|
106
|
+
Fields: [
|
|
107
|
+
{ Name: 'jobid', DisplayName: 'Job ID', Type: 'number', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Upload job identifier.' },
|
|
108
|
+
{ Name: 'status', DisplayName: 'Status', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Job status.' },
|
|
109
|
+
{ Name: 'total_rows', DisplayName: 'Total Rows', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Total rows in CSV.' },
|
|
110
|
+
{ Name: 'processed_rows', DisplayName: 'Processed Rows', Type: 'number', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Number of rows processed.' },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
// ─── Connector Implementation ────────────────────────────────────────
|
|
115
|
+
/**
|
|
116
|
+
* Connector for Higher Logic MagnetMail (mmapi.asmx SOAP API).
|
|
117
|
+
*
|
|
118
|
+
* Extends BaseIntegrationConnector directly because MagnetMail is SOAP/XML.
|
|
119
|
+
* The generic BaseRESTIntegrationConnector pagination helpers don't apply;
|
|
120
|
+
* MagnetMail has per-operation offset/row_count pagination, UTC-variant names,
|
|
121
|
+
* and envelope-wrapped responses that need XML parsing.
|
|
122
|
+
*
|
|
123
|
+
* Operations are driven by `IntegrationObject` metadata:
|
|
124
|
+
* - `APIPath` holds the SOAP action name for list/read (e.g., `getMessagesUTC`)
|
|
125
|
+
* - `ResponseDataKey` holds the response element wrapping the result (e.g., `getMessagesUTCResult`)
|
|
126
|
+
* - `DefaultQueryParams` carries additional metadata:
|
|
127
|
+
* - `create_action` / `update_action` / `delete_action` / `detail_action` — SOAP action names
|
|
128
|
+
* - `watermark_field` / `watermark_from_param` / `watermark_to_param` — incremental sync settings
|
|
129
|
+
*
|
|
130
|
+
* Auth flow:
|
|
131
|
+
* 1. POST `Authenticate` with userId + password -> session token
|
|
132
|
+
* 2. Attach (user_id, session) to every subsequent SOAP call
|
|
133
|
+
* 3. Cache session for SESSION_TTL_MS; refresh on expiry or auth failure
|
|
134
|
+
*/
|
|
135
|
+
let MagnetMailConnector = class MagnetMailConnector extends BaseIntegrationConnector {
|
|
136
|
+
constructor() {
|
|
137
|
+
// ── State ────────────────────────────────────────────────────────
|
|
138
|
+
super(...arguments);
|
|
139
|
+
this.cachedSession = null;
|
|
140
|
+
this.lastRequestTime = 0;
|
|
141
|
+
}
|
|
142
|
+
// ── Capability Getters ───────────────────────────────────────────
|
|
143
|
+
get SupportsCreate() { return true; }
|
|
144
|
+
get SupportsUpdate() { return true; }
|
|
145
|
+
get SupportsDelete() { return true; }
|
|
146
|
+
get SupportsSearch() { return false; }
|
|
147
|
+
get SupportsListing() { return false; }
|
|
148
|
+
get IntegrationName() { return 'MagnetMail'; }
|
|
149
|
+
// ── Action Generation ────────────────────────────────────────────
|
|
150
|
+
GetIntegrationObjects() {
|
|
151
|
+
return MAGNETMAIL_OBJECTS;
|
|
152
|
+
}
|
|
153
|
+
GetActionGeneratorConfig() {
|
|
154
|
+
const objects = this.GetIntegrationObjects();
|
|
155
|
+
if (objects.length === 0)
|
|
156
|
+
return null;
|
|
157
|
+
return {
|
|
158
|
+
IntegrationName: 'MagnetMail',
|
|
159
|
+
CategoryName: 'MagnetMail',
|
|
160
|
+
IconClass: 'fa-solid fa-envelope-circle-check',
|
|
161
|
+
Objects: objects,
|
|
162
|
+
IncludeSearch: false,
|
|
163
|
+
IncludeList: false,
|
|
164
|
+
CategoryDescription: 'Higher Logic MagnetMail email marketing integration actions',
|
|
165
|
+
ParentCategoryName: 'Business Apps',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// ── Default Configuration ────────────────────────────────────────
|
|
169
|
+
GetDefaultConfiguration() {
|
|
170
|
+
return {
|
|
171
|
+
DefaultSchemaName: 'MagnetMail',
|
|
172
|
+
DefaultObjects: [
|
|
173
|
+
{
|
|
174
|
+
SourceObjectName: 'Recipients',
|
|
175
|
+
TargetTableName: 'MagnetMail_Recipient',
|
|
176
|
+
TargetEntityName: 'MagnetMail Recipients',
|
|
177
|
+
SyncEnabled: true,
|
|
178
|
+
FieldMappings: this.GetDefaultFieldMappings('Recipients', 'Contacts'),
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
SourceObjectName: 'Groups',
|
|
182
|
+
TargetTableName: 'MagnetMail_Group',
|
|
183
|
+
TargetEntityName: 'MagnetMail Groups',
|
|
184
|
+
SyncEnabled: true,
|
|
185
|
+
FieldMappings: [],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
SourceObjectName: 'Messages',
|
|
189
|
+
TargetTableName: 'MagnetMail_Message',
|
|
190
|
+
TargetEntityName: 'MagnetMail Messages',
|
|
191
|
+
SyncEnabled: true,
|
|
192
|
+
FieldMappings: [],
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
GetDefaultFieldMappings(objectName, _entityName) {
|
|
198
|
+
if (objectName === 'Recipients') {
|
|
199
|
+
return [
|
|
200
|
+
{ SourceFieldName: 'recipient_id', DestinationFieldName: 'ExternalID', IsKeyField: true },
|
|
201
|
+
{ SourceFieldName: 'email', DestinationFieldName: 'Email' },
|
|
202
|
+
{ SourceFieldName: 'first_name', DestinationFieldName: 'FirstName' },
|
|
203
|
+
{ SourceFieldName: 'last_name', DestinationFieldName: 'LastName' },
|
|
204
|
+
{ SourceFieldName: 'phone', DestinationFieldName: 'Phone' },
|
|
205
|
+
{ SourceFieldName: 'company', DestinationFieldName: 'Company' },
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
// ── TestConnection ───────────────────────────────────────────────
|
|
211
|
+
/**
|
|
212
|
+
* Exercises the Authenticate operation and then calls getUserDetails to
|
|
213
|
+
* validate the session token is accepted by a regular endpoint.
|
|
214
|
+
*/
|
|
215
|
+
async TestConnection(companyIntegration, contextUser) {
|
|
216
|
+
try {
|
|
217
|
+
const session = await this.GetSession(companyIntegration, contextUser, true);
|
|
218
|
+
const response = await this.InvokeSoapOperation(session, 'getUserDetails', {});
|
|
219
|
+
const account = this.ExtractNodeText(response, 'account_name') ?? 'authenticated';
|
|
220
|
+
return {
|
|
221
|
+
Success: true,
|
|
222
|
+
Message: `Successfully authenticated to MagnetMail (user_id=${session.Config.UserId}, account=${account})`,
|
|
223
|
+
ServerVersion: 'MagnetMail mmapi.asmx (SOAP 1.1/1.2)',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
228
|
+
return { Success: false, Message: `Connection failed: ${message}` };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// ── Discovery ────────────────────────────────────────────────────
|
|
232
|
+
/**
|
|
233
|
+
* Returns static metadata — MagnetMail's WSDL is fixed and no runtime
|
|
234
|
+
* discovery is possible for SOAP operations. Engine's cached IntegrationObjects
|
|
235
|
+
* are returned when available; otherwise the connector's static list.
|
|
236
|
+
*/
|
|
237
|
+
async DiscoverObjects(companyIntegration, _contextUser) {
|
|
238
|
+
const cached = IntegrationEngineBase.Instance.GetActiveIntegrationObjects(companyIntegration.IntegrationID);
|
|
239
|
+
if (cached && cached.length > 0) {
|
|
240
|
+
return cached.map(obj => ({
|
|
241
|
+
ID: obj.ID,
|
|
242
|
+
Name: obj.Name,
|
|
243
|
+
Label: obj.DisplayName ?? obj.Name,
|
|
244
|
+
Description: obj.Description ?? undefined,
|
|
245
|
+
SupportsIncrementalSync: obj.SupportsIncrementalSync,
|
|
246
|
+
SupportsWrite: obj.SupportsWrite,
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
return MAGNETMAIL_OBJECTS.map(o => ({
|
|
250
|
+
Name: o.Name,
|
|
251
|
+
Label: o.DisplayName,
|
|
252
|
+
Description: o.Description,
|
|
253
|
+
SupportsIncrementalSync: o.Name === 'Messages' || o.Name === 'Unsubscribes' || o.Name === 'MessageTrackingDetailed',
|
|
254
|
+
SupportsWrite: o.SupportsWrite,
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
async DiscoverFields(companyIntegration, objectName, _contextUser) {
|
|
258
|
+
const cached = IntegrationEngineBase.Instance.GetIntegrationObject(companyIntegration.IntegrationID, objectName);
|
|
259
|
+
if (cached) {
|
|
260
|
+
const fields = IntegrationEngineBase.Instance.GetIntegrationObjectFields(cached.ID);
|
|
261
|
+
return fields.map(f => ({
|
|
262
|
+
Name: f.Name,
|
|
263
|
+
Label: f.DisplayName ?? f.Name,
|
|
264
|
+
Description: f.Description ?? undefined,
|
|
265
|
+
DataType: f.Type,
|
|
266
|
+
IsRequired: f.IsRequired,
|
|
267
|
+
IsUniqueKey: f.IsUniqueKey || f.IsPrimaryKey,
|
|
268
|
+
IsReadOnly: f.IsReadOnly,
|
|
269
|
+
IsForeignKey: f.RelatedIntegrationObjectID != null,
|
|
270
|
+
ForeignKeyTarget: f.RelatedIntegrationObject ?? null,
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
const obj = MAGNETMAIL_OBJECTS.find(o => o.Name === objectName);
|
|
274
|
+
if (!obj)
|
|
275
|
+
return [];
|
|
276
|
+
return obj.Fields.map(f => ({
|
|
277
|
+
Name: f.Name,
|
|
278
|
+
Label: f.DisplayName,
|
|
279
|
+
Description: f.Description,
|
|
280
|
+
DataType: f.Type,
|
|
281
|
+
IsRequired: f.IsRequired,
|
|
282
|
+
IsUniqueKey: f.IsPrimaryKey,
|
|
283
|
+
IsReadOnly: f.IsReadOnly,
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
// ── FetchChanges ─────────────────────────────────────────────────
|
|
287
|
+
/**
|
|
288
|
+
* Drives a list/read SOAP operation using the object's metadata. Pagination
|
|
289
|
+
* uses MagnetMail's `start_row` / `row_count` convention; incremental sync
|
|
290
|
+
* passes watermark_from/to parameters when configured on the IntegrationObject.
|
|
291
|
+
*/
|
|
292
|
+
async FetchChanges(ctx) {
|
|
293
|
+
const obj = this.ResolveObject(ctx.CompanyIntegration.IntegrationID, ctx.ObjectName);
|
|
294
|
+
const meta = this.BuildObjectMeta(obj);
|
|
295
|
+
const session = await this.GetSession(ctx.CompanyIntegration, ctx.ContextUser);
|
|
296
|
+
const offset = ctx.CurrentOffset ?? 0;
|
|
297
|
+
const pageSize = ctx.BatchSize || obj.DefaultPageSize || 500;
|
|
298
|
+
const args = this.BuildFetchArgs(meta, ctx, offset, pageSize);
|
|
299
|
+
const responseXml = await this.InvokeSoapOperation(session, meta.ListAction, args);
|
|
300
|
+
const records = this.ParseRecordArray(responseXml, meta.ResultPath);
|
|
301
|
+
const pkField = this.GetPrimaryKeyField(obj.ID);
|
|
302
|
+
const externalRecords = records.map(r => this.RawToExternalRecord(r, ctx.ObjectName, pkField));
|
|
303
|
+
const watermarkAfter = meta.WatermarkField ? this.ComputeWatermark(records, meta.WatermarkField) : undefined;
|
|
304
|
+
return {
|
|
305
|
+
Records: externalRecords,
|
|
306
|
+
HasMore: obj.SupportsPagination && records.length >= pageSize,
|
|
307
|
+
NewWatermarkValue: watermarkAfter,
|
|
308
|
+
NextOffset: records.length >= pageSize ? offset + records.length : undefined,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
// ── CRUD operations ──────────────────────────────────────────────
|
|
312
|
+
async CreateRecord(ctx) {
|
|
313
|
+
return this.ExecuteMutation(ctx, 'CreateAction', ctx.Attributes, null);
|
|
314
|
+
}
|
|
315
|
+
async UpdateRecord(ctx) {
|
|
316
|
+
return this.ExecuteMutation(ctx, 'UpdateAction', ctx.Attributes, ctx.ExternalID);
|
|
317
|
+
}
|
|
318
|
+
async DeleteRecord(ctx) {
|
|
319
|
+
return this.ExecuteMutation(ctx, 'DeleteAction', {}, ctx.ExternalID);
|
|
320
|
+
}
|
|
321
|
+
async GetRecord(ctx) {
|
|
322
|
+
const companyIntegration = ctx.CompanyIntegration;
|
|
323
|
+
const contextUser = ctx.ContextUser;
|
|
324
|
+
const obj = this.ResolveObject(companyIntegration.IntegrationID, ctx.ObjectName);
|
|
325
|
+
const meta = this.BuildObjectMeta(obj);
|
|
326
|
+
const action = meta.DetailAction;
|
|
327
|
+
if (!action)
|
|
328
|
+
return null;
|
|
329
|
+
const session = await this.GetSession(companyIntegration, contextUser);
|
|
330
|
+
const pkField = this.GetPrimaryKeyField(obj.ID);
|
|
331
|
+
const args = { ...meta.ExtraArgs, [pkField]: ctx.ExternalID };
|
|
332
|
+
const responseXml = await this.InvokeSoapOperation(session, action, args);
|
|
333
|
+
const records = this.ParseRecordArray(responseXml, `${action}Result`);
|
|
334
|
+
if (records.length === 0)
|
|
335
|
+
return null;
|
|
336
|
+
return this.RawToExternalRecord(records[0], ctx.ObjectName, pkField);
|
|
337
|
+
}
|
|
338
|
+
// ── Mutation helper ──────────────────────────────────────────────
|
|
339
|
+
/**
|
|
340
|
+
* Shared create/update/delete implementation. Reads the mutation SOAP action
|
|
341
|
+
* name from the IntegrationObject's DefaultQueryParams metadata and POSTs
|
|
342
|
+
* the given attributes.
|
|
343
|
+
*/
|
|
344
|
+
async ExecuteMutation(ctx, metaKey, attributes, externalID) {
|
|
345
|
+
const companyIntegration = ctx.CompanyIntegration;
|
|
346
|
+
const contextUser = ctx.ContextUser;
|
|
347
|
+
const obj = this.ResolveObject(companyIntegration.IntegrationID, ctx.ObjectName);
|
|
348
|
+
const meta = this.BuildObjectMeta(obj);
|
|
349
|
+
const actionName = this.GetMutationActionName(meta, metaKey);
|
|
350
|
+
if (!actionName) {
|
|
351
|
+
return {
|
|
352
|
+
Success: false,
|
|
353
|
+
ErrorMessage: `${metaKey} is not supported for MagnetMail object ${ctx.ObjectName}`,
|
|
354
|
+
StatusCode: 400,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
const session = await this.GetSession(companyIntegration, contextUser);
|
|
359
|
+
const args = this.BuildMutationArgs(meta, attributes, externalID, obj);
|
|
360
|
+
const responseXml = await this.InvokeSoapOperation(session, actionName, args);
|
|
361
|
+
const returnedId = this.ExtractMutationResultId(responseXml, actionName, obj) ?? externalID ?? '';
|
|
362
|
+
return { Success: true, ExternalID: returnedId, StatusCode: 200 };
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
return this.BuildCRUDError(err, metaKey, ctx.ObjectName);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
GetMutationActionName(meta, key) {
|
|
369
|
+
if (key === 'CreateAction')
|
|
370
|
+
return meta.CreateAction;
|
|
371
|
+
if (key === 'UpdateAction')
|
|
372
|
+
return meta.UpdateAction;
|
|
373
|
+
return meta.DeleteAction;
|
|
374
|
+
}
|
|
375
|
+
BuildMutationArgs(meta, attributes, externalID, obj) {
|
|
376
|
+
const args = { ...meta.ExtraArgs };
|
|
377
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
378
|
+
if (value == null)
|
|
379
|
+
continue;
|
|
380
|
+
args[key] = String(value);
|
|
381
|
+
}
|
|
382
|
+
if (externalID != null) {
|
|
383
|
+
const pkField = this.GetPrimaryKeyField(obj.ID);
|
|
384
|
+
args[pkField] = externalID;
|
|
385
|
+
}
|
|
386
|
+
return args;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Attempts to extract an ID from a create/update response. MagnetMail
|
|
390
|
+
* conventions vary per operation; this falls back to returning the
|
|
391
|
+
* external ID already passed in.
|
|
392
|
+
*/
|
|
393
|
+
ExtractMutationResultId(responseXml, action, obj) {
|
|
394
|
+
const pkField = this.GetPrimaryKeyField(obj.ID);
|
|
395
|
+
const direct = this.ExtractNodeText(responseXml, pkField);
|
|
396
|
+
if (direct)
|
|
397
|
+
return direct;
|
|
398
|
+
const resultPath = `${action}Result`;
|
|
399
|
+
const wrapped = this.ExtractWrappedNodeText(responseXml, resultPath, pkField);
|
|
400
|
+
if (wrapped)
|
|
401
|
+
return wrapped;
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
// ── Session Management ───────────────────────────────────────────
|
|
405
|
+
async GetSession(companyIntegration, contextUser, forceRefresh = false) {
|
|
406
|
+
if (!forceRefresh && this.cachedSession && this.IsSessionValid()) {
|
|
407
|
+
return this.cachedSession;
|
|
408
|
+
}
|
|
409
|
+
const config = await this.ParseConfig(companyIntegration, contextUser);
|
|
410
|
+
const token = await this.Authenticate(config);
|
|
411
|
+
const session = {
|
|
412
|
+
SessionToken: token,
|
|
413
|
+
CreatedAt: Date.now(),
|
|
414
|
+
Config: config,
|
|
415
|
+
};
|
|
416
|
+
this.cachedSession = session;
|
|
417
|
+
return session;
|
|
418
|
+
}
|
|
419
|
+
IsSessionValid() {
|
|
420
|
+
if (!this.cachedSession)
|
|
421
|
+
return false;
|
|
422
|
+
const ttl = this.cachedSession.Config.SessionTTLMs ?? DEFAULT_SESSION_TTL_MS;
|
|
423
|
+
return Date.now() - this.cachedSession.CreatedAt < ttl;
|
|
424
|
+
}
|
|
425
|
+
async Authenticate(config) {
|
|
426
|
+
// Vendor WSDL: Authenticate accepts {username, password} (NOT user_id).
|
|
427
|
+
const envelope = this.BuildEnvelope(config, 'Authenticate', {
|
|
428
|
+
username: config.UserId,
|
|
429
|
+
password: config.Password,
|
|
430
|
+
}, /* includeSession */ false);
|
|
431
|
+
const responseXml = await this.PostSOAP(config, 'Authenticate', envelope);
|
|
432
|
+
// Vendor returns <sessionId> (capital S, capital I). Try AuthenticateResult
|
|
433
|
+
// first (some operations wrap in an *Result envelope), then sessionId, and
|
|
434
|
+
// fall back to lowercase variants for older/forked deployments.
|
|
435
|
+
const token = this.ExtractNodeText(responseXml, 'AuthenticateResult')
|
|
436
|
+
?? this.ExtractNodeText(responseXml, 'sessionId')
|
|
437
|
+
?? this.ExtractNodeText(responseXml, 'session')
|
|
438
|
+
?? this.ExtractNodeText(responseXml, 'sessionid');
|
|
439
|
+
if (!token) {
|
|
440
|
+
throw new Error('MagnetMailConnector: Authenticate did not return a session token');
|
|
441
|
+
}
|
|
442
|
+
return token;
|
|
443
|
+
}
|
|
444
|
+
// ── Configuration Parsing ────────────────────────────────────────
|
|
445
|
+
async ParseConfig(companyIntegration, contextUser) {
|
|
446
|
+
const credentialID = companyIntegration.CredentialID;
|
|
447
|
+
if (credentialID) {
|
|
448
|
+
const config = await this.LoadFromCredentialEntity(credentialID, contextUser);
|
|
449
|
+
if (config)
|
|
450
|
+
return config;
|
|
451
|
+
}
|
|
452
|
+
const configJson = companyIntegration.Configuration;
|
|
453
|
+
if (configJson) {
|
|
454
|
+
const parsed = JSON.parse(configJson);
|
|
455
|
+
return this.ValidateConfig(parsed);
|
|
456
|
+
}
|
|
457
|
+
throw new Error('MagnetMailConnector: No credentials or configuration found on CompanyIntegration');
|
|
458
|
+
}
|
|
459
|
+
async LoadFromCredentialEntity(credentialID, contextUser, provider) {
|
|
460
|
+
const md = provider ?? new Metadata();
|
|
461
|
+
const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
|
|
462
|
+
const loaded = await credential.Load(credentialID);
|
|
463
|
+
if (!loaded || !credential.Values)
|
|
464
|
+
return null;
|
|
465
|
+
try {
|
|
466
|
+
const raw = JSON.parse(credential.Values);
|
|
467
|
+
const parsed = {
|
|
468
|
+
UserId: raw.userId ?? raw.UserId,
|
|
469
|
+
Password: raw.password ?? raw.Password,
|
|
470
|
+
Endpoint: raw.endpoint ?? raw.Endpoint,
|
|
471
|
+
Namespace: raw.namespace ?? raw.Namespace,
|
|
472
|
+
};
|
|
473
|
+
return this.ValidateConfig(parsed);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
ValidateConfig(raw) {
|
|
480
|
+
if (!raw.UserId)
|
|
481
|
+
throw new Error('MagnetMailConnector: UserId is required');
|
|
482
|
+
if (!raw.Password)
|
|
483
|
+
throw new Error('MagnetMailConnector: Password is required');
|
|
484
|
+
return {
|
|
485
|
+
UserId: raw.UserId,
|
|
486
|
+
Password: raw.Password,
|
|
487
|
+
Endpoint: raw.Endpoint ?? DEFAULT_ENDPOINT,
|
|
488
|
+
Namespace: raw.Namespace ?? DEFAULT_NAMESPACE,
|
|
489
|
+
MaxRetries: raw.MaxRetries ?? DEFAULT_MAX_RETRIES,
|
|
490
|
+
RequestTimeoutMs: raw.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
491
|
+
MinRequestIntervalMs: raw.MinRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS,
|
|
492
|
+
SessionTTLMs: raw.SessionTTLMs ?? DEFAULT_SESSION_TTL_MS,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
// ── SOAP helpers ─────────────────────────────────────────────────
|
|
496
|
+
/**
|
|
497
|
+
* Invokes a SOAP operation with session authentication. The `args` map is
|
|
498
|
+
* serialised as child elements of the operation element; complex types
|
|
499
|
+
* should be pre-serialised to string by callers.
|
|
500
|
+
*/
|
|
501
|
+
async InvokeSoapOperation(session, action, args) {
|
|
502
|
+
const envelope = this.BuildEnvelope(session.Config, action, args, /* includeSession */ true, session.SessionToken);
|
|
503
|
+
return this.PostSOAP(session.Config, action, envelope);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Builds a SOAP 1.1 envelope. Vendor WSDL places authentication in a SOAP
|
|
507
|
+
* HEADER element <mmAuthHeader> containing <sessionId> + <user_id> — NOT in
|
|
508
|
+
* the operation body. The operation body carries only operation-specific args.
|
|
509
|
+
*/
|
|
510
|
+
BuildEnvelope(config, operation, args, includeSession, sessionToken) {
|
|
511
|
+
const namespace = config.Namespace ?? DEFAULT_NAMESPACE;
|
|
512
|
+
const headerBlock = includeSession
|
|
513
|
+
? ` <soap:Header>\n <mmAuthHeader xmlns="${namespace}">\n <sessionId>${this.EscapeXmlValue(sessionToken ?? '')}</sessionId>\n <user_id>${this.EscapeXmlValue(config.UserId)}</user_id>\n </mmAuthHeader>\n </soap:Header>\n`
|
|
514
|
+
: '';
|
|
515
|
+
// Operations whose WSDL wraps all arguments in a single complex element.
|
|
516
|
+
// searchForRecipients is the canonical one: all filter fields go INSIDE
|
|
517
|
+
// a <criteria> element rather than as flat children of the operation.
|
|
518
|
+
const COMPLEX_WRAPPERS = {
|
|
519
|
+
'searchForRecipients': 'criteria',
|
|
520
|
+
};
|
|
521
|
+
const wrapper = COMPLEX_WRAPPERS[operation];
|
|
522
|
+
const renderArgs = (entries) => entries.map(([k, v]) => `<${k}>${this.EscapeXmlValue(v)}</${k}>`).join('\n ');
|
|
523
|
+
let body;
|
|
524
|
+
if (wrapper) {
|
|
525
|
+
const inner = renderArgs(Object.entries(args));
|
|
526
|
+
body = ` <${wrapper}>\n ${inner}\n </${wrapper}>`;
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
body = ` ${renderArgs(Object.entries(args))}`;
|
|
530
|
+
}
|
|
531
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
532
|
+
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
|
533
|
+
${headerBlock} <soap:Body>
|
|
534
|
+
<${operation} xmlns="${namespace}">
|
|
535
|
+
${body}
|
|
536
|
+
</${operation}>
|
|
537
|
+
</soap:Body>
|
|
538
|
+
</soap:Envelope>`;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* POSTs a SOAP envelope with throttling, retry, and timeout.
|
|
542
|
+
*/
|
|
543
|
+
async PostSOAP(config, action, envelope) {
|
|
544
|
+
const endpoint = config.Endpoint ?? DEFAULT_ENDPOINT;
|
|
545
|
+
const namespace = config.Namespace ?? DEFAULT_NAMESPACE;
|
|
546
|
+
const soapAction = `${namespace}${action}`;
|
|
547
|
+
const maxRetries = config.MaxRetries ?? DEFAULT_MAX_RETRIES;
|
|
548
|
+
const timeoutMs = config.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
549
|
+
const minInterval = config.MinRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS;
|
|
550
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
551
|
+
await this.Throttle(minInterval);
|
|
552
|
+
const outcome = await this.ExecuteSoapAttempt(endpoint, envelope, soapAction, timeoutMs);
|
|
553
|
+
if (outcome.retryable && attempt < maxRetries) {
|
|
554
|
+
const delay = Math.min(Math.pow(2, attempt) * 1000, 15000);
|
|
555
|
+
await this.Sleep(delay);
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (outcome.error)
|
|
559
|
+
throw outcome.error;
|
|
560
|
+
return outcome.xml;
|
|
561
|
+
}
|
|
562
|
+
throw new Error(`MagnetMailConnector: exhausted ${maxRetries + 1} attempts for SOAP action "${action}"`);
|
|
563
|
+
}
|
|
564
|
+
async ExecuteSoapAttempt(endpoint, envelope, soapAction, timeoutMs) {
|
|
565
|
+
const controller = new AbortController();
|
|
566
|
+
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
|
|
567
|
+
try {
|
|
568
|
+
const response = await fetch(endpoint, {
|
|
569
|
+
method: 'POST',
|
|
570
|
+
headers: {
|
|
571
|
+
'Content-Type': 'text/xml; charset=utf-8',
|
|
572
|
+
'SOAPAction': `"${soapAction}"`,
|
|
573
|
+
'Accept': 'text/xml',
|
|
574
|
+
},
|
|
575
|
+
body: envelope,
|
|
576
|
+
signal: controller.signal,
|
|
577
|
+
});
|
|
578
|
+
this.lastRequestTime = Date.now();
|
|
579
|
+
const xml = await response.text();
|
|
580
|
+
if (response.status === 429 || response.status >= 500) {
|
|
581
|
+
return { error: new Error(`MagnetMail HTTP ${response.status}: ${xml.slice(0, 500)}`), retryable: true };
|
|
582
|
+
}
|
|
583
|
+
if (!response.ok) {
|
|
584
|
+
return { error: new Error(`MagnetMail HTTP ${response.status}: ${xml.slice(0, 500)}`), retryable: false };
|
|
585
|
+
}
|
|
586
|
+
this.CheckSoapFault(xml);
|
|
587
|
+
return { xml, retryable: false };
|
|
588
|
+
}
|
|
589
|
+
catch (err) {
|
|
590
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
591
|
+
const retryable = message.includes('abort') || message.includes('timeout') || message.includes('ECONNRESET');
|
|
592
|
+
return { error: new Error(`MagnetMail SOAP request failed: ${message}`), retryable };
|
|
593
|
+
}
|
|
594
|
+
finally {
|
|
595
|
+
clearTimeout(timeoutHandle);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// ── XML parsing (regex-based, consistent with SageIntacct approach) ──
|
|
599
|
+
/**
|
|
600
|
+
* Throws a descriptive error if the SOAP response contains a fault element.
|
|
601
|
+
*/
|
|
602
|
+
CheckSoapFault(xml) {
|
|
603
|
+
const faultMatch = xml.match(/<soap:Fault>([\s\S]*?)<\/soap:Fault>/i);
|
|
604
|
+
if (!faultMatch)
|
|
605
|
+
return;
|
|
606
|
+
const fault = faultMatch[1];
|
|
607
|
+
const code = this.ExtractNodeText(fault, 'faultcode') ?? 'UNKNOWN';
|
|
608
|
+
const reason = this.ExtractNodeText(fault, 'faultstring') ?? this.ExtractNodeText(fault, 'Reason') ?? 'unknown';
|
|
609
|
+
throw new Error(`MagnetMail SOAP Fault ${code}: ${reason}`);
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Extracts the text content of the first element with the given tag name.
|
|
613
|
+
* Namespace-agnostic — matches by local name.
|
|
614
|
+
*/
|
|
615
|
+
ExtractNodeText(xml, tag) {
|
|
616
|
+
const regex = new RegExp(`<(?:\\w+:)?${this.EscapeRegex(tag)}[^>]*>([\\s\\S]*?)<\\/(?:\\w+:)?${this.EscapeRegex(tag)}>`, 'i');
|
|
617
|
+
const match = xml.match(regex);
|
|
618
|
+
return match ? this.DecodeXmlEntities(match[1].trim()) : null;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Extracts a node nested inside another wrapper node (e.g., `<FooResult><id>42</id></FooResult>`).
|
|
622
|
+
*/
|
|
623
|
+
ExtractWrappedNodeText(xml, wrapperTag, innerTag) {
|
|
624
|
+
const wrapperRegex = new RegExp(`<(?:\\w+:)?${this.EscapeRegex(wrapperTag)}[^>]*>([\\s\\S]*?)<\\/(?:\\w+:)?${this.EscapeRegex(wrapperTag)}>`, 'i');
|
|
625
|
+
const wrapperMatch = xml.match(wrapperRegex);
|
|
626
|
+
if (!wrapperMatch)
|
|
627
|
+
return null;
|
|
628
|
+
return this.ExtractNodeText(wrapperMatch[1], innerTag);
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Parses an array of records inside a result wrapper. Returns one plain
|
|
632
|
+
* object per record element. Works for both singular wrapper responses
|
|
633
|
+
* (e.g., GetMessageDetails) and array-style (e.g., getMessagesUTC).
|
|
634
|
+
*/
|
|
635
|
+
ParseRecordArray(xml, resultPath) {
|
|
636
|
+
const containerXml = resultPath ? this.FindElementContent(xml, resultPath) : xml;
|
|
637
|
+
if (!containerXml)
|
|
638
|
+
return [];
|
|
639
|
+
const childElements = this.ExtractDirectChildElements(containerXml);
|
|
640
|
+
if (childElements.length === 0)
|
|
641
|
+
return [];
|
|
642
|
+
// Group consecutive same-tag elements as a record array
|
|
643
|
+
const records = [];
|
|
644
|
+
for (const child of childElements) {
|
|
645
|
+
records.push(this.ParseElementFields(child.content));
|
|
646
|
+
}
|
|
647
|
+
return records;
|
|
648
|
+
}
|
|
649
|
+
FindElementContent(xml, tag) {
|
|
650
|
+
const regex = new RegExp(`<(?:\\w+:)?${this.EscapeRegex(tag)}[^>]*>([\\s\\S]*?)<\\/(?:\\w+:)?${this.EscapeRegex(tag)}>`, 'i');
|
|
651
|
+
const match = xml.match(regex);
|
|
652
|
+
return match ? match[1] : null;
|
|
653
|
+
}
|
|
654
|
+
ExtractDirectChildElements(xml) {
|
|
655
|
+
const results = [];
|
|
656
|
+
const regex = /<(?:\w+:)?(\w+)(?:\s[^>]*)?>([\s\S]*?)<\/(?:\w+:)?\1>/g;
|
|
657
|
+
let match;
|
|
658
|
+
while ((match = regex.exec(xml)) !== null) {
|
|
659
|
+
results.push({ tag: match[1], content: match[2] });
|
|
660
|
+
}
|
|
661
|
+
return results;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Parses a record element's children into a flat key-value map. Nested
|
|
665
|
+
* objects and arrays are returned as raw inner XML strings (caller can
|
|
666
|
+
* re-parse if needed — simple enough for most MagnetMail fields).
|
|
667
|
+
*/
|
|
668
|
+
ParseElementFields(xml) {
|
|
669
|
+
const record = {};
|
|
670
|
+
const children = this.ExtractDirectChildElements(xml);
|
|
671
|
+
for (const child of children) {
|
|
672
|
+
const value = this.CoerceFieldValue(child.content.trim());
|
|
673
|
+
// Last-write-wins for repeated child elements; MagnetMail rarely emits them in these shapes
|
|
674
|
+
record[child.tag] = value;
|
|
675
|
+
}
|
|
676
|
+
return record;
|
|
677
|
+
}
|
|
678
|
+
CoerceFieldValue(raw) {
|
|
679
|
+
if (raw === '')
|
|
680
|
+
return null;
|
|
681
|
+
if (raw === 'true')
|
|
682
|
+
return true;
|
|
683
|
+
if (raw === 'false')
|
|
684
|
+
return false;
|
|
685
|
+
if (/^-?\d+$/.test(raw))
|
|
686
|
+
return parseInt(raw, 10);
|
|
687
|
+
if (/^-?\d+\.\d+$/.test(raw))
|
|
688
|
+
return parseFloat(raw);
|
|
689
|
+
return this.DecodeXmlEntities(raw);
|
|
690
|
+
}
|
|
691
|
+
// ── Metadata helpers ─────────────────────────────────────────────
|
|
692
|
+
ResolveObject(integrationID, objectName) {
|
|
693
|
+
const obj = IntegrationEngineBase.Instance.GetIntegrationObject(integrationID, objectName);
|
|
694
|
+
if (!obj)
|
|
695
|
+
throw new Error(`MagnetMailConnector: unknown IntegrationObject "${objectName}"`);
|
|
696
|
+
return obj;
|
|
697
|
+
}
|
|
698
|
+
BuildObjectMeta(obj) {
|
|
699
|
+
const listAction = obj.APIPath || '';
|
|
700
|
+
if (!listAction)
|
|
701
|
+
throw new Error(`MagnetMailConnector: IntegrationObject "${obj.Name}" is missing APIPath (SOAP action)`);
|
|
702
|
+
const resultPath = obj.ResponseDataKey || `${listAction}Result`;
|
|
703
|
+
const raw = this.ParseDefaultQueryParams(obj);
|
|
704
|
+
const extraArgs = {};
|
|
705
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
706
|
+
if (RESERVED_META_KEYS.has(k))
|
|
707
|
+
continue;
|
|
708
|
+
extraArgs[k] = String(v);
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
ListAction: listAction,
|
|
712
|
+
ResultPath: resultPath,
|
|
713
|
+
CreateAction: raw['create_action'],
|
|
714
|
+
UpdateAction: raw['update_action'],
|
|
715
|
+
DeleteAction: raw['delete_action'],
|
|
716
|
+
DetailAction: raw['detail_action'],
|
|
717
|
+
WatermarkField: raw['watermark_field'],
|
|
718
|
+
WatermarkFromParam: raw['watermark_from_param'],
|
|
719
|
+
WatermarkToParam: raw['watermark_to_param'],
|
|
720
|
+
ExtraArgs: extraArgs,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
ParseDefaultQueryParams(obj) {
|
|
724
|
+
if (!obj.DefaultQueryParams)
|
|
725
|
+
return {};
|
|
726
|
+
try {
|
|
727
|
+
return JSON.parse(obj.DefaultQueryParams);
|
|
728
|
+
}
|
|
729
|
+
catch {
|
|
730
|
+
console.warn(`[MagnetMailConnector] Invalid DefaultQueryParams JSON for "${obj.Name}"`);
|
|
731
|
+
return {};
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
GetPrimaryKeyField(objectID) {
|
|
735
|
+
const fields = IntegrationEngineBase.Instance.GetIntegrationObjectFields(objectID);
|
|
736
|
+
const pk = fields.find(f => f.IsPrimaryKey);
|
|
737
|
+
return pk ? pk.Name : 'id';
|
|
738
|
+
}
|
|
739
|
+
BuildFetchArgs(meta, ctx, offset, pageSize) {
|
|
740
|
+
const args = { ...meta.ExtraArgs };
|
|
741
|
+
// Vendor WSDL: pagination uses 1-based pageNumber + pageCount on the
|
|
742
|
+
// operations that accept pagination. Some operations (searchForRecipients,
|
|
743
|
+
// getMessagesUTC) accept NO pagination — meta.SupportsPagination guards.
|
|
744
|
+
if (meta.SupportsPagination !== false) {
|
|
745
|
+
const pageNumber = Math.floor(offset / Math.max(1, pageSize)) + 1;
|
|
746
|
+
args['pageNumber'] = String(pageNumber);
|
|
747
|
+
args['pageCount'] = String(pageSize);
|
|
748
|
+
}
|
|
749
|
+
if (ctx.WatermarkValue && meta.WatermarkFromParam) {
|
|
750
|
+
args[meta.WatermarkFromParam] = ctx.WatermarkValue;
|
|
751
|
+
}
|
|
752
|
+
return args;
|
|
753
|
+
}
|
|
754
|
+
RawToExternalRecord(raw, objectType, pkField) {
|
|
755
|
+
const id = raw[pkField];
|
|
756
|
+
return {
|
|
757
|
+
ExternalID: id != null ? String(id) : '',
|
|
758
|
+
ObjectType: objectType,
|
|
759
|
+
Fields: raw,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
ComputeWatermark(records, field) {
|
|
763
|
+
let latest;
|
|
764
|
+
for (const record of records) {
|
|
765
|
+
const value = record[field];
|
|
766
|
+
if (value && typeof value === 'string' && (!latest || value > latest)) {
|
|
767
|
+
latest = value;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return latest;
|
|
771
|
+
}
|
|
772
|
+
// ── Utility helpers ──────────────────────────────────────────────
|
|
773
|
+
async Throttle(minIntervalMs) {
|
|
774
|
+
const elapsed = Date.now() - this.lastRequestTime;
|
|
775
|
+
if (elapsed < minIntervalMs) {
|
|
776
|
+
await this.Sleep(minIntervalMs - elapsed);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
Sleep(ms) {
|
|
780
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
781
|
+
}
|
|
782
|
+
EscapeXmlValue(value) {
|
|
783
|
+
return value
|
|
784
|
+
.replace(/&/g, '&')
|
|
785
|
+
.replace(/</g, '<')
|
|
786
|
+
.replace(/>/g, '>')
|
|
787
|
+
.replace(/"/g, '"')
|
|
788
|
+
.replace(/'/g, ''');
|
|
789
|
+
}
|
|
790
|
+
EscapeRegex(value) {
|
|
791
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
792
|
+
}
|
|
793
|
+
DecodeXmlEntities(value) {
|
|
794
|
+
return value
|
|
795
|
+
.replace(/</g, '<')
|
|
796
|
+
.replace(/>/g, '>')
|
|
797
|
+
.replace(/"/g, '"')
|
|
798
|
+
.replace(/'/g, "'")
|
|
799
|
+
.replace(/&/g, '&');
|
|
800
|
+
}
|
|
801
|
+
BuildCRUDError(err, operation, objectName) {
|
|
802
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
803
|
+
return {
|
|
804
|
+
Success: false,
|
|
805
|
+
ErrorMessage: `${operation} failed for ${objectName}: ${message}`,
|
|
806
|
+
StatusCode: 500,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
MagnetMailConnector = __decorate([
|
|
811
|
+
RegisterClass(BaseIntegrationConnector, 'MagnetMailConnector')
|
|
812
|
+
], MagnetMailConnector);
|
|
813
|
+
export { MagnetMailConnector };
|
|
814
|
+
/** Tree-shaking prevention function — import and call from module entry point. */
|
|
815
|
+
export function LoadMagnetMailConnector() { }
|
|
816
|
+
//# sourceMappingURL=MagnetMailConnector.js.map
|