@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,745 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { RegisterClass } from '@memberjunction/global';
8
+ import { Metadata } from '@memberjunction/core';
9
+ import { BaseIntegrationConnector, BaseRESTIntegrationConnector, } from '@memberjunction/integration-engine';
10
+ // ─── Constants ───────────────────────────────────────────────────────
11
+ /** Default maximum retries */
12
+ const DEFAULT_MAX_RETRIES = 3;
13
+ /** Default HTTP timeout (ms) */
14
+ const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
15
+ /** Default minimum interval between requests (ms) — GrowthZone recommends 2 seconds */
16
+ const DEFAULT_MIN_REQUEST_INTERVAL_MS = 2_000;
17
+ /** Default page size for flat list endpoints */
18
+ const DEFAULT_PAGE_SIZE = 500;
19
+ /** Sentinel date used by GrowthZone to represent null datetimes */
20
+ const GROWTHZONE_NULL_DATE = '0001-01-01T00:00:00';
21
+ /** Names of nested array fields to flatten from OrgGeneral */
22
+ const ORG_NESTED_ARRAYS = [
23
+ 'Activities',
24
+ 'Categories',
25
+ 'Certifications',
26
+ 'Communication',
27
+ 'Memberships',
28
+ 'SalesOpportunities',
29
+ ];
30
+ // ─── Action-metadata objects (mirrors .growthzone.json) ──────────────
31
+ const GROWTHZONE_ACTION_OBJECTS = [
32
+ {
33
+ Name: 'contacts', DisplayName: 'Contact',
34
+ Description: 'A contact (person or organization) in GrowthZone',
35
+ SupportsWrite: false,
36
+ Fields: [
37
+ { Name: 'ContactId', DisplayName: 'Contact ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'GrowthZone contact identifier' },
38
+ { Name: 'DisplayName', DisplayName: 'Display Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Formatted display name' },
39
+ { Name: 'ModifiedDate', DisplayName: 'Modified Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Last modification timestamp' },
40
+ { Name: 'IsDeleted', DisplayName: 'Is Deleted', Type: 'boolean', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Soft-delete flag' },
41
+ { Name: 'SystemContactTypeId', DisplayName: 'Contact Type', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Individual (1) or Organization (2)' },
42
+ ],
43
+ },
44
+ {
45
+ Name: 'memberships', DisplayName: 'Membership',
46
+ Description: 'A membership held by a contact in GrowthZone',
47
+ SupportsWrite: false,
48
+ Fields: [
49
+ { Name: 'MembershipId', DisplayName: 'Membership ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Membership identifier' },
50
+ { Name: 'ContactId', DisplayName: 'Contact ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Holder contact' },
51
+ { Name: 'MembershipTypeId', DisplayName: 'Membership Type ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Membership type/package' },
52
+ { Name: 'MembershipTypeName', DisplayName: 'Membership Type Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Type name' },
53
+ { Name: 'MembershipStatusTypeId', DisplayName: 'Status', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Status code (0-9)' },
54
+ { Name: 'StartDate', DisplayName: 'Start Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Effective start date' },
55
+ { Name: 'EndDate', DisplayName: 'End Date', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Expiration date' },
56
+ ],
57
+ },
58
+ {
59
+ Name: 'groups', DisplayName: 'Group',
60
+ Description: 'A group/committee in GrowthZone', SupportsWrite: false,
61
+ Fields: [
62
+ { Name: 'GroupId', DisplayName: 'Group ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Group identifier' },
63
+ { Name: 'Name', DisplayName: 'Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Group name' },
64
+ { Name: 'Description', DisplayName: 'Description', Type: 'text', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Group description' },
65
+ { Name: 'IsActive', DisplayName: 'Is Active', Type: 'boolean', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Active flag' },
66
+ ],
67
+ },
68
+ {
69
+ Name: 'events', DisplayName: 'Event',
70
+ Description: 'An event in GrowthZone', SupportsWrite: false,
71
+ Fields: [
72
+ { Name: 'EventId', DisplayName: 'Event ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Event identifier' },
73
+ { Name: 'Name', DisplayName: 'Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Event name' },
74
+ { Name: 'StartDate', DisplayName: 'Start Date', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Start datetime' },
75
+ { Name: 'EndDate', DisplayName: 'End Date', Type: 'datetime', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'End datetime' },
76
+ { Name: 'Location', DisplayName: 'Location', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Event location' },
77
+ { Name: 'IsActive', DisplayName: 'Is Active', Type: 'boolean', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Active flag' },
78
+ ],
79
+ },
80
+ {
81
+ Name: 'invoices', DisplayName: 'Invoice',
82
+ Description: 'An invoice in GrowthZone billing', SupportsWrite: false,
83
+ Fields: [
84
+ { Name: 'InvoiceId', DisplayName: 'Invoice ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Invoice identifier' },
85
+ { Name: 'ContactId', DisplayName: 'Contact ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Billed contact' },
86
+ { Name: 'InvoiceNumber', DisplayName: 'Invoice Number', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'User-visible invoice number' },
87
+ { Name: 'TotalAmount', DisplayName: 'Total Amount', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Invoice total' },
88
+ { Name: 'BalanceDue', DisplayName: 'Balance Due', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Outstanding balance' },
89
+ { Name: 'Status', DisplayName: 'Status', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Invoice status' },
90
+ ],
91
+ },
92
+ {
93
+ Name: 'payments', DisplayName: 'Payment',
94
+ Description: 'A payment record in GrowthZone', SupportsWrite: false,
95
+ Fields: [
96
+ { Name: 'PaymentId', DisplayName: 'Payment ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Payment identifier' },
97
+ { Name: 'ContactId', DisplayName: 'Contact ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Paying contact' },
98
+ { Name: 'Amount', DisplayName: 'Amount', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Payment amount' },
99
+ { Name: 'PaymentMethod', DisplayName: 'Payment Method', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Method (Card, ACH, Check, Cash)' },
100
+ { Name: 'PaymentStatus', DisplayName: 'Payment Status', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Status (Pending, Captured, Refunded)' },
101
+ ],
102
+ },
103
+ {
104
+ Name: 'certifications', DisplayName: 'Certification',
105
+ Description: 'A certification held by a contact', SupportsWrite: false,
106
+ Fields: [
107
+ { Name: 'CertificationContactId', DisplayName: 'Certification Contact ID', Type: 'string', IsRequired: true, IsReadOnly: true, IsPrimaryKey: true, Description: 'Unique record identifier' },
108
+ { Name: 'ContactId', DisplayName: 'Contact ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Holder contact' },
109
+ { Name: 'CertificationId', DisplayName: 'Certification ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Certification definition' },
110
+ { Name: 'Name', DisplayName: 'Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Certification name' },
111
+ { Name: 'Status', DisplayName: 'Status', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Status (InProgress, Completed, Expired)' },
112
+ ],
113
+ },
114
+ ];
115
+ // ─── Connector Implementation ────────────────────────────────────────
116
+ /**
117
+ * Connector for the GrowthZone association management REST API.
118
+ *
119
+ * Extends BaseRESTIntegrationConnector to leverage metadata-driven object/field
120
+ * discovery from IntegrationEngineBase cache plus generic pagination.
121
+ *
122
+ * Auth: sends `Authorization: ApiKey {key}` on every request. API keys are
123
+ * provisioned by GrowthZone WebSupport. Tenant subdomain is required to
124
+ * resolve the base URL.
125
+ *
126
+ * Pagination: GrowthZone uses OData-style params on flat endpoints —
127
+ * `$skip`, `$top`, and `$orderby`. `$filter`, `$expand`, `$select` are NOT
128
+ * supported. The only incremental endpoint is `/api/contacts/delta`, which
129
+ * accepts `?modifiedSince=ISO8601&top=N`.
130
+ *
131
+ * Throttle: GrowthZone recommends 2s minimum interval; the connector defaults
132
+ * to that. Responds to HTTP 429/503 with exponential backoff.
133
+ */
134
+ let GrowthZoneConnector = class GrowthZoneConnector extends BaseRESTIntegrationConnector {
135
+ constructor() {
136
+ super(...arguments);
137
+ /** Cached auth context for the current sync run */
138
+ this.authCache = null;
139
+ /** Timestamp of the last API request, for throttling */
140
+ this.lastRequestTime = 0;
141
+ /**
142
+ * Object name currently being fetched — tracked so BuildPaginatedURL can
143
+ * emit endpoint-specific query parameters (e.g., `modifiedSince` for delta).
144
+ */
145
+ this.currentObjectName = '';
146
+ }
147
+ get IntegrationName() { return 'GrowthZone'; }
148
+ GetIntegrationObjects() {
149
+ return GROWTHZONE_ACTION_OBJECTS;
150
+ }
151
+ GetActionGeneratorConfig() {
152
+ const objects = this.GetIntegrationObjects();
153
+ if (objects.length === 0)
154
+ return null;
155
+ return {
156
+ IntegrationName: 'GrowthZone',
157
+ CategoryName: 'GrowthZone',
158
+ IconClass: 'fa-solid fa-seedling',
159
+ Objects: objects,
160
+ IncludeSearch: false,
161
+ IncludeList: false,
162
+ CategoryDescription: 'GrowthZone association management integration for contacts, memberships, events, and billing',
163
+ ParentCategoryName: 'Association Management',
164
+ };
165
+ }
166
+ // ── Capability getters ───────────────────────────────────────────
167
+ get SupportsCreate() { return false; }
168
+ get SupportsUpdate() { return false; }
169
+ get SupportsDelete() { return false; }
170
+ // ── Default mappings ─────────────────────────────────────────────
171
+ GetDefaultConfiguration() {
172
+ return {
173
+ DefaultSchemaName: 'GrowthZone',
174
+ DefaultObjects: [],
175
+ };
176
+ }
177
+ GetDefaultFieldMappings(objectName, _entityName) {
178
+ const lower = objectName.toLowerCase();
179
+ if (lower === 'contacts' || lower === 'contact-person') {
180
+ return [
181
+ { SourceFieldName: 'ContactId', DestinationFieldName: 'ExternalID', IsKeyField: true },
182
+ { SourceFieldName: 'DisplayName', DestinationFieldName: 'Name' },
183
+ { SourceFieldName: 'FirstName', DestinationFieldName: 'FirstName' },
184
+ { SourceFieldName: 'LastName', DestinationFieldName: 'LastName' },
185
+ { SourceFieldName: 'IsDeleted', DestinationFieldName: 'IsDeleted' },
186
+ { SourceFieldName: 'ModifiedDate', DestinationFieldName: 'UpdatedAt' },
187
+ ];
188
+ }
189
+ if (lower === 'contact-org') {
190
+ return [
191
+ { SourceFieldName: 'ContactId', DestinationFieldName: 'ExternalID', IsKeyField: true },
192
+ { SourceFieldName: 'OrganizationName', DestinationFieldName: 'Name' },
193
+ { SourceFieldName: 'ModifiedDate', DestinationFieldName: 'UpdatedAt' },
194
+ ];
195
+ }
196
+ if (lower === 'memberships') {
197
+ return [
198
+ { SourceFieldName: 'MembershipId', DestinationFieldName: 'ExternalID', IsKeyField: true },
199
+ { SourceFieldName: 'ContactId', DestinationFieldName: 'ContactExternalID' },
200
+ { SourceFieldName: 'MembershipStatusTypeId', DestinationFieldName: 'Status' },
201
+ { SourceFieldName: 'StartDate', DestinationFieldName: 'StartDate' },
202
+ { SourceFieldName: 'EndDate', DestinationFieldName: 'EndDate' },
203
+ ];
204
+ }
205
+ return [];
206
+ }
207
+ // ─── BaseRESTIntegrationConnector abstract methods ──────────────
208
+ async Authenticate(companyIntegration, contextUser) {
209
+ if (this.authCache)
210
+ return this.authCache;
211
+ const config = await this.ParseConfig(companyIntegration, contextUser);
212
+ const baseUrl = this.ResolveBaseUrl(config);
213
+ const auth = { Token: config.ApiKey, BaseUrl: baseUrl, Config: config };
214
+ this.authCache = auth;
215
+ return auth;
216
+ }
217
+ BuildHeaders(auth) {
218
+ const token = auth.Token ?? '';
219
+ return {
220
+ 'Authorization': `ApiKey ${token}`,
221
+ 'Accept': 'application/json',
222
+ 'Content-Type': 'application/json',
223
+ };
224
+ }
225
+ GetBaseURL(_companyIntegration, auth) {
226
+ return auth.BaseUrl;
227
+ }
228
+ /**
229
+ * Extracts records from GrowthZone responses. Handles three shapes:
230
+ * 1. `{ Results: [...], TotalRecordAvailable: N }` — paginated lists
231
+ * 2. Raw array — some endpoints return arrays at the root
232
+ * 3. Single object — detail endpoints (per-contact fetches)
233
+ *
234
+ * Normalizes `0001-01-01` dates and empty strings to null.
235
+ */
236
+ NormalizeResponse(rawBody, responseDataKey) {
237
+ if (rawBody == null)
238
+ return [];
239
+ if (Array.isArray(rawBody)) {
240
+ return rawBody.map(r => this.NormalizeRecord(r));
241
+ }
242
+ if (typeof rawBody === 'object') {
243
+ const body = rawBody;
244
+ // Paginated envelope: { Results: [...], TotalRecordAvailable: N }
245
+ const envelope = body;
246
+ if (Array.isArray(envelope.Results)) {
247
+ return envelope.Results.map(r => this.NormalizeRecord(r));
248
+ }
249
+ // Custom response key from metadata
250
+ if (responseDataKey && Array.isArray(body[responseDataKey])) {
251
+ const arr = body[responseDataKey];
252
+ return arr.map(r => this.NormalizeRecord(r));
253
+ }
254
+ // Nested flattening cases for OrgGeneral/NotesAndFields
255
+ const flattened = this.MaybeFlattenNested(body);
256
+ if (flattened)
257
+ return flattened;
258
+ // Single detail record
259
+ return [this.NormalizeRecord(body)];
260
+ }
261
+ return [];
262
+ }
263
+ /**
264
+ * Extracts pagination state. GrowthZone uses OData pagination with
265
+ * `$skip` and `$top`; there's no `HasMore` flag — we derive it from
266
+ * `TotalRecordAvailable` vs. current offset + returned count.
267
+ */
268
+ ExtractPaginationInfo(rawBody, _paginationType, _currentPage, currentOffset, pageSize) {
269
+ if (!rawBody || typeof rawBody !== 'object') {
270
+ return { HasMore: false };
271
+ }
272
+ const body = rawBody;
273
+ const results = Array.isArray(body.Results) ? body.Results : [];
274
+ const total = typeof body.TotalRecordAvailable === 'number'
275
+ ? body.TotalRecordAvailable
276
+ : undefined;
277
+ // Fewer records than requested = end of stream
278
+ if (results.length < pageSize) {
279
+ return { HasMore: false, TotalRecords: total };
280
+ }
281
+ // Check against total count if provided
282
+ const nextOffset = currentOffset + results.length;
283
+ if (total !== undefined && nextOffset >= total) {
284
+ return { HasMore: false, TotalRecords: total };
285
+ }
286
+ return {
287
+ HasMore: true,
288
+ NextOffset: nextOffset,
289
+ TotalRecords: total,
290
+ };
291
+ }
292
+ /**
293
+ * Overrides the base pagination URL builder to emit GrowthZone-specific
294
+ * OData parameters (`$skip`, `$top`) and, for the delta contacts endpoint,
295
+ * the `modifiedSince` watermark parameter.
296
+ *
297
+ * Driven entirely by IntegrationObject metadata — no hardcoded object-name
298
+ * switches beyond the delta-specific param which is keyed on APIPath.
299
+ */
300
+ BuildPaginatedURL(basePath, obj, _page, offset, _cursor, effectivePageSize) {
301
+ const limit = effectivePageSize ?? obj.DefaultPageSize ?? DEFAULT_PAGE_SIZE;
302
+ const params = new URLSearchParams();
303
+ // GrowthZone's only incremental endpoint: /api/contacts/delta
304
+ const isDelta = obj.APIPath?.toLowerCase().endsWith('/delta') ?? false;
305
+ if (isDelta && this.currentWatermark) {
306
+ params.set('modifiedSince', this.currentWatermark);
307
+ params.set('top', String(limit));
308
+ if (offset > 0)
309
+ params.set('skip', String(offset));
310
+ }
311
+ else if (obj.SupportsPagination && obj.PaginationType === 'Offset') {
312
+ params.set('$top', String(limit));
313
+ if (offset > 0)
314
+ params.set('$skip', String(offset));
315
+ }
316
+ const qs = params.toString();
317
+ return qs ? `${basePath}?${qs}` : basePath;
318
+ }
319
+ /**
320
+ * Executes an HTTP request with rate limiting and retry for 429/503.
321
+ */
322
+ async MakeHTTPRequest(auth, url, method, headers, body) {
323
+ const gzAuth = auth;
324
+ const maxRetries = gzAuth.Config.MaxRetries ?? DEFAULT_MAX_RETRIES;
325
+ const timeoutMs = gzAuth.Config.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
326
+ const minInterval = gzAuth.Config.MinRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS;
327
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
328
+ await this.ThrottleIfNeeded(minInterval);
329
+ const fetchOptions = {
330
+ method,
331
+ headers,
332
+ signal: AbortSignal.timeout(timeoutMs),
333
+ };
334
+ if (body !== undefined && method !== 'GET') {
335
+ fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
336
+ }
337
+ let response;
338
+ try {
339
+ response = await fetch(url, fetchOptions);
340
+ }
341
+ catch (err) {
342
+ if (attempt < maxRetries && this.IsTransientNetworkError(err)) {
343
+ await this.Sleep(this.BackoffDelay(attempt));
344
+ continue;
345
+ }
346
+ throw err;
347
+ }
348
+ this.lastRequestTime = Date.now();
349
+ if ((response.status === 429 || response.status === 503) && attempt < maxRetries) {
350
+ console.warn(`[GrowthZone] HTTP ${response.status} from ${url} — backing off`);
351
+ await this.Sleep(this.BackoffDelay(attempt));
352
+ continue;
353
+ }
354
+ return this.BuildRESTResponse(response);
355
+ }
356
+ throw new Error(`GrowthZone request failed after ${maxRetries + 1} attempts: ${url}`);
357
+ }
358
+ // ─── TestConnection ──────────────────────────────────────────────
359
+ /**
360
+ * Tests connectivity by calling the delta endpoint with a recent
361
+ * watermark. A 200 response confirms that the tenant and API key are
362
+ * valid.
363
+ */
364
+ async TestConnection(companyIntegration, contextUser) {
365
+ try {
366
+ const config = await this.ParseConfig(companyIntegration, contextUser);
367
+ const baseUrl = this.ResolveBaseUrl(config);
368
+ const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
369
+ const url = `${baseUrl}/contacts/delta?modifiedSince=${encodeURIComponent(since)}&top=1`;
370
+ const response = await fetch(url, {
371
+ method: 'GET',
372
+ headers: {
373
+ 'Authorization': `ApiKey ${config.ApiKey}`,
374
+ 'Accept': 'application/json',
375
+ },
376
+ signal: AbortSignal.timeout(DEFAULT_REQUEST_TIMEOUT_MS),
377
+ });
378
+ if (!response.ok) {
379
+ const preview = (await response.text()).slice(0, 300);
380
+ return {
381
+ Success: false,
382
+ Message: `GrowthZone returned HTTP ${response.status} from ${url}: ${preview}`,
383
+ };
384
+ }
385
+ return {
386
+ Success: true,
387
+ Message: `Connected to GrowthZone tenant '${config.Tenant}'`,
388
+ ServerVersion: 'GrowthZone REST API',
389
+ };
390
+ }
391
+ catch (err) {
392
+ const message = err instanceof Error ? err.message : String(err);
393
+ return { Success: false, Message: `Connection failed: ${message}` };
394
+ }
395
+ }
396
+ // ─── Discovery with runtime custom fields ───────────────────────
397
+ /**
398
+ * Discovers objects from the cache. GrowthZone's dynamic capability is
399
+ * limited to custom fields on contacts (not custom objects/tables) —
400
+ * so DiscoverObjects only returns the metadata-seeded baseline.
401
+ */
402
+ async DiscoverObjects(companyIntegration, contextUser) {
403
+ return super.DiscoverObjects(companyIntegration, contextUser);
404
+ }
405
+ /**
406
+ * Discovers fields for a given object, augmenting the cached metadata
407
+ * with runtime-discovered custom fields for contact objects. Custom
408
+ * fields get IsCustom=true via IntegrationSchemaSync when persisted.
409
+ */
410
+ async DiscoverFields(companyIntegration, objectName, contextUser) {
411
+ const staticFields = await super.DiscoverFields(companyIntegration, objectName, contextUser);
412
+ // Custom-field discovery only applies to the contact-custom-field-values
413
+ // and custom-field-definitions objects.
414
+ const lower = objectName.toLowerCase();
415
+ if (lower !== 'custom-field-definitions' && lower !== 'contact-custom-field-values') {
416
+ return staticFields;
417
+ }
418
+ try {
419
+ const customFields = await this.FetchCustomFieldDefinitions(companyIntegration, contextUser);
420
+ const discovered = customFields.map(cf => this.CustomFieldToSchema(cf));
421
+ return [...staticFields, ...discovered];
422
+ }
423
+ catch (err) {
424
+ const message = err instanceof Error ? err.message : String(err);
425
+ console.warn(`[GrowthZone] Runtime custom-field discovery failed: ${message}`);
426
+ return staticFields;
427
+ }
428
+ }
429
+ /**
430
+ * Fetches custom field definitions from GrowthZone. Uses a sample contact
431
+ * ID to query NotesAndFields — since the curated API does not expose a
432
+ * global custom-fields list endpoint, a sample contact is used to probe
433
+ * the defined fields.
434
+ */
435
+ async FetchCustomFieldDefinitions(companyIntegration, contextUser) {
436
+ const auth = (await this.Authenticate(companyIntegration, contextUser));
437
+ const headers = this.BuildHeaders(auth);
438
+ // Canonical vendor endpoint: POST /api/customfields/getall returns ALL
439
+ // tenant-defined custom field definitions across all object types in one
440
+ // call. Documented at documentation.growthzoneapp.com/CustomField.html.
441
+ // Previously this method probed per-contact NotesAndFields, which was
442
+ // both slow and limited to fields actually attached to the probed record.
443
+ try {
444
+ const url = `${auth.BaseUrl}/customfields/getall`;
445
+ const response = await this.MakeHTTPRequest(auth, url, 'POST', headers, {});
446
+ if (response.Status >= 200 && response.Status < 300) {
447
+ const body = response.Body;
448
+ const items = Array.isArray(body) ? body : (body.Results ?? []);
449
+ if (items.length > 0)
450
+ return items;
451
+ }
452
+ }
453
+ catch {
454
+ // Fall through to legacy per-contact probe if the global endpoint isn't
455
+ // available on the tenant (e.g., older deployments may not expose it).
456
+ }
457
+ // Legacy fallback: probe a single contact's NotesAndFields. Kept so older
458
+ // GrowthZone deployments without /customfields/getall continue to work.
459
+ const since = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString();
460
+ const listUrl = `${auth.BaseUrl}/contacts/delta?modifiedSince=${encodeURIComponent(since)}&top=1`;
461
+ const listResp = await this.MakeHTTPRequest(auth, listUrl, 'GET', headers);
462
+ if (listResp.Status < 200 || listResp.Status >= 300)
463
+ return [];
464
+ const listBody = listResp.Body;
465
+ const contacts = Array.isArray(listBody) ? listBody : (listBody.Results ?? []);
466
+ if (contacts.length === 0)
467
+ return [];
468
+ const probeId = String(contacts[0].ContactId);
469
+ const fieldsUrl = `${auth.BaseUrl}/contacts/${encodeURIComponent(probeId)}/NotesAndFields`;
470
+ const fieldsResp = await this.MakeHTTPRequest(auth, fieldsUrl, 'GET', headers);
471
+ if (fieldsResp.Status < 200 || fieldsResp.Status >= 300)
472
+ return [];
473
+ const notesAndFields = fieldsResp.Body;
474
+ return notesAndFields.Fields ?? [];
475
+ }
476
+ /**
477
+ * Maps a GrowthZone custom field definition to an ExternalFieldSchema.
478
+ */
479
+ CustomFieldToSchema(cf) {
480
+ return {
481
+ Name: `CustomField_${cf.CustomFieldId}`,
482
+ Label: cf.DisplayName ?? cf.Name ?? `Custom Field ${cf.CustomFieldId}`,
483
+ Description: cf.Description ?? undefined,
484
+ DataType: this.MapCustomFieldType(cf.CustomFieldType),
485
+ IsRequired: cf.IsRequired === true,
486
+ IsUniqueKey: false,
487
+ IsReadOnly: cf.IsArchived === true,
488
+ IsForeignKey: false,
489
+ };
490
+ }
491
+ /**
492
+ * Maps GrowthZone custom-field type to generic integration type.
493
+ */
494
+ MapCustomFieldType(cfType) {
495
+ const t = (cfType ?? '').toLowerCase();
496
+ if (t === 'number')
497
+ return 'decimal';
498
+ if (t === 'date')
499
+ return 'datetime';
500
+ if (t === 'selectlist' || t === 'scale')
501
+ return 'string';
502
+ if (t === 'file')
503
+ return 'string';
504
+ return 'string';
505
+ }
506
+ // ─── FetchChanges override ──────────────────────────────────────
507
+ /**
508
+ * Override to store watermark/object-name context used by BuildPaginatedURL
509
+ * and to advance the watermark from returned records.
510
+ */
511
+ async FetchChanges(ctx) {
512
+ this.currentObjectName = ctx.ObjectName;
513
+ this.currentWatermark = ctx.WatermarkValue ?? undefined;
514
+ const result = await super.FetchChanges(ctx);
515
+ const isFinal = !result.HasMore;
516
+ const newWatermark = isFinal
517
+ ? (this.ExtractLatestModifiedDate(result.Records) ?? ctx.WatermarkValue ?? undefined)
518
+ : undefined;
519
+ return {
520
+ ...result,
521
+ NewWatermarkValue: newWatermark,
522
+ };
523
+ }
524
+ // ─── Private helpers ────────────────────────────────────────────
525
+ /**
526
+ * Normalizes a raw record: strips GrowthZone null-sentinel dates and
527
+ * treats empty strings as null. Does NOT recurse into nested objects
528
+ * beyond the top level — the base class handles that.
529
+ */
530
+ NormalizeRecord(record) {
531
+ const out = {};
532
+ for (const [key, value] of Object.entries(record)) {
533
+ out[key] = this.NormalizeValue(value);
534
+ }
535
+ return out;
536
+ }
537
+ /** Converts GrowthZone sentinel values to null. */
538
+ NormalizeValue(value) {
539
+ if (value == null)
540
+ return null;
541
+ if (typeof value === 'string') {
542
+ if (value === '')
543
+ return null;
544
+ if (value.startsWith(GROWTHZONE_NULL_DATE))
545
+ return null;
546
+ return value;
547
+ }
548
+ return value;
549
+ }
550
+ /**
551
+ * For OrgGeneral / NotesAndFields responses, the current object being
552
+ * fetched may request a flattened nested array rather than the root
553
+ * record. Returns flattened rows when currentObjectName matches a
554
+ * known nested-array target, else null.
555
+ */
556
+ MaybeFlattenNested(body) {
557
+ const name = this.currentObjectName.toLowerCase();
558
+ // NotesAndFields — flatten Fields[] array for custom field values
559
+ if (name === 'contact-custom-field-values' && Array.isArray(body.Fields)) {
560
+ const fields = body.Fields;
561
+ const contactId = body.ContactId ?? body.contactId;
562
+ return fields.map(cf => this.NormalizeRecord({
563
+ ContactId: contactId ?? '',
564
+ CustomFieldId: cf.CustomFieldId,
565
+ Name: cf.Name ?? null,
566
+ DisplayName: cf.DisplayName ?? null,
567
+ CustomFieldType: cf.CustomFieldType ?? null,
568
+ Value: this.StringifyCustomFieldValue(cf.Value),
569
+ FieldGroupName: cf.FieldGroupName ?? null,
570
+ IsPublic: cf.IsPublic ?? null,
571
+ IsArchived: cf.IsArchived ?? null,
572
+ }));
573
+ }
574
+ // OrgGeneral — flatten one of the nested arrays based on current object
575
+ const nestedMap = {
576
+ 'contact-activities': 'Activities',
577
+ 'contact-categories': 'Categories',
578
+ 'contact-communications': 'Communication',
579
+ 'contact-sales-opportunities': 'SalesOpportunities',
580
+ };
581
+ const arrKey = nestedMap[name];
582
+ if (arrKey && Array.isArray(body[arrKey])) {
583
+ const rows = body[arrKey];
584
+ const contactId = body.ContactId ?? body.contactId;
585
+ return rows.map(row => this.NormalizeRecord({
586
+ ContactId: contactId ?? '',
587
+ ...row,
588
+ }));
589
+ }
590
+ return null;
591
+ }
592
+ /** Stringifies custom-field values so they fit nvarchar storage. */
593
+ StringifyCustomFieldValue(value) {
594
+ if (value == null)
595
+ return null;
596
+ if (typeof value === 'string')
597
+ return value;
598
+ if (typeof value === 'number' || typeof value === 'boolean')
599
+ return String(value);
600
+ try {
601
+ return JSON.stringify(value);
602
+ }
603
+ catch {
604
+ return null;
605
+ }
606
+ }
607
+ /**
608
+ * Extracts the latest ModifiedDate from a batch of records for
609
+ * watermark advancement.
610
+ */
611
+ ExtractLatestModifiedDate(records) {
612
+ let latest = null;
613
+ for (const rec of records) {
614
+ const dateStr = rec.Fields?.ModifiedDate;
615
+ if (typeof dateStr !== 'string' || dateStr.length === 0)
616
+ continue;
617
+ const d = new Date(dateStr);
618
+ if (!isNaN(d.getTime()) && (latest === null || d > latest)) {
619
+ latest = d;
620
+ }
621
+ }
622
+ return latest ? latest.toISOString() : null;
623
+ }
624
+ /**
625
+ * Parses connection config, preferring the attached credential entity
626
+ * over the raw Configuration JSON.
627
+ */
628
+ async ParseConfig(companyIntegration, contextUser) {
629
+ if (companyIntegration.CredentialID) {
630
+ return this.ParseConfigFromCredential(companyIntegration.CredentialID, contextUser);
631
+ }
632
+ if (companyIntegration.Configuration) {
633
+ return this.ValidateConfig(JSON.parse(companyIntegration.Configuration));
634
+ }
635
+ throw new Error('GrowthZone connector requires either CredentialID or Configuration JSON');
636
+ }
637
+ /** Loads config from the MJ: Credentials entity. */
638
+ async ParseConfigFromCredential(credentialID, contextUser, provider) {
639
+ const md = provider ?? new Metadata();
640
+ const cred = await md.GetEntityObject('MJ: Credentials', contextUser);
641
+ await cred.Load(credentialID);
642
+ if (!cred.Values)
643
+ throw new Error('GrowthZone credential has no Values JSON');
644
+ return this.ValidateConfig(JSON.parse(cred.Values));
645
+ }
646
+ /** Validates and applies defaults to the config. */
647
+ ValidateConfig(raw) {
648
+ if (!raw || typeof raw !== 'object') {
649
+ throw new Error('GrowthZone configuration is not a valid object');
650
+ }
651
+ const get = (key) => {
652
+ const obj = raw;
653
+ const lower = key.toLowerCase();
654
+ for (const [k, v] of Object.entries(obj)) {
655
+ if (k.toLowerCase() === lower && typeof v === 'string')
656
+ return v;
657
+ }
658
+ return undefined;
659
+ };
660
+ const getNum = (key) => {
661
+ const obj = raw;
662
+ const lower = key.toLowerCase();
663
+ for (const [k, v] of Object.entries(obj)) {
664
+ if (k.toLowerCase() === lower && typeof v === 'number')
665
+ return v;
666
+ }
667
+ return undefined;
668
+ };
669
+ const tenant = get('tenant') ?? get('subdomain');
670
+ const apiKey = get('apikey') ?? get('api_key') ?? get('key');
671
+ const baseUrlOverride = get('baseurloverride') ?? get('base_url_override') ?? get('baseurl');
672
+ if (!tenant && !baseUrlOverride) {
673
+ throw new Error('GrowthZone configuration missing required field: Tenant (or BaseUrlOverride)');
674
+ }
675
+ if (!apiKey) {
676
+ throw new Error('GrowthZone configuration missing required field: ApiKey');
677
+ }
678
+ return {
679
+ Tenant: tenant ?? '',
680
+ ApiKey: apiKey,
681
+ BaseUrlOverride: baseUrlOverride,
682
+ MaxRetries: getNum('maxretries') ?? DEFAULT_MAX_RETRIES,
683
+ RequestTimeoutMs: getNum('requesttimeoutms') ?? DEFAULT_REQUEST_TIMEOUT_MS,
684
+ MinRequestIntervalMs: getNum('minrequestintervalms') ?? DEFAULT_MIN_REQUEST_INTERVAL_MS,
685
+ };
686
+ }
687
+ /**
688
+ * Resolves the base URL from either an override or the tenant subdomain.
689
+ * Always returns a URL ending in `/api` with no trailing slash.
690
+ */
691
+ ResolveBaseUrl(config) {
692
+ if (config.BaseUrlOverride) {
693
+ const trimmed = config.BaseUrlOverride.replace(/\/+$/, '');
694
+ return trimmed.endsWith('/api') ? trimmed : `${trimmed}/api`;
695
+ }
696
+ return `https://${config.Tenant}.growthzoneapp.com/api`;
697
+ }
698
+ /** Throttle to respect GrowthZone's recommended minimum request interval. */
699
+ async ThrottleIfNeeded(minIntervalMs) {
700
+ const elapsed = Date.now() - this.lastRequestTime;
701
+ if (elapsed < minIntervalMs) {
702
+ await this.Sleep(minIntervalMs - elapsed);
703
+ }
704
+ }
705
+ /** Exponential backoff delay for retry attempts. */
706
+ BackoffDelay(attempt) {
707
+ return Math.min(2_000 * Math.pow(2, attempt), 30_000);
708
+ }
709
+ /** Checks whether an error is transient (network/timeout). */
710
+ IsTransientNetworkError(err) {
711
+ if (!(err instanceof Error))
712
+ return false;
713
+ const msg = err.message.toLowerCase();
714
+ return msg.includes('timeout') || msg.includes('abort') ||
715
+ msg.includes('econnreset') || msg.includes('econnrefused') ||
716
+ msg.includes('fetch failed');
717
+ }
718
+ /** Builds the normalized RESTResponse from a fetch Response. */
719
+ async BuildRESTResponse(response) {
720
+ const headers = {};
721
+ response.headers.forEach((v, k) => { headers[k.toLowerCase()] = v; });
722
+ const text = await response.text();
723
+ let body = null;
724
+ if (text.length > 0) {
725
+ try {
726
+ body = JSON.parse(text);
727
+ }
728
+ catch {
729
+ body = text;
730
+ }
731
+ }
732
+ return { Status: response.status, Body: body, Headers: headers };
733
+ }
734
+ /** Promise-wrapped setTimeout. */
735
+ Sleep(ms) {
736
+ return new Promise(resolve => setTimeout(resolve, ms));
737
+ }
738
+ };
739
+ GrowthZoneConnector = __decorate([
740
+ RegisterClass(BaseIntegrationConnector, 'GrowthZoneConnector')
741
+ ], GrowthZoneConnector);
742
+ export { GrowthZoneConnector };
743
+ /** Tree-shaking prevention function — import and call from module entry point */
744
+ export function LoadGrowthZoneConnector() { }
745
+ //# sourceMappingURL=GrowthZoneConnector.js.map