@memberjunction/integration-connectors 5.51.0 → 6.1.0-edge.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/LICENSE +7 -0
- package/README.md +109 -277
- package/dist/index.d.ts +0 -36
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -38
- package/dist/index.js.map +1 -1
- package/package.json +14 -19
- package/dist/AptifyConnector.d.ts +0 -95
- package/dist/AptifyConnector.d.ts.map +0 -1
- package/dist/AptifyConnector.js +0 -760
- package/dist/AptifyConnector.js.map +0 -1
- package/dist/BlackbaudConnector.d.ts +0 -64
- package/dist/BlackbaudConnector.d.ts.map +0 -1
- package/dist/BlackbaudConnector.js +0 -916
- package/dist/BlackbaudConnector.js.map +0 -1
- package/dist/ConstantContactConnector.d.ts +0 -137
- package/dist/ConstantContactConnector.d.ts.map +0 -1
- package/dist/ConstantContactConnector.js +0 -663
- package/dist/ConstantContactConnector.js.map +0 -1
- package/dist/CventConnector.d.ts +0 -146
- package/dist/CventConnector.d.ts.map +0 -1
- package/dist/CventConnector.js +0 -540
- package/dist/CventConnector.js.map +0 -1
- package/dist/DynamicsDataverseConnector.d.ts +0 -296
- package/dist/DynamicsDataverseConnector.d.ts.map +0 -1
- package/dist/DynamicsDataverseConnector.js +0 -790
- package/dist/DynamicsDataverseConnector.js.map +0 -1
- package/dist/FileFeedConnector.d.ts +0 -64
- package/dist/FileFeedConnector.d.ts.map +0 -1
- package/dist/FileFeedConnector.js +0 -190
- package/dist/FileFeedConnector.js.map +0 -1
- package/dist/FontevaConnector.d.ts +0 -153
- package/dist/FontevaConnector.d.ts.map +0 -1
- package/dist/FontevaConnector.js +0 -421
- package/dist/FontevaConnector.js.map +0 -1
- package/dist/GrowthZoneConnector.d.ts +0 -183
- package/dist/GrowthZoneConnector.d.ts.map +0 -1
- package/dist/GrowthZoneConnector.js +0 -702
- package/dist/GrowthZoneConnector.js.map +0 -1
- package/dist/HivebriteConnector.d.ts +0 -151
- package/dist/HivebriteConnector.d.ts.map +0 -1
- package/dist/HivebriteConnector.js +0 -533
- package/dist/HivebriteConnector.js.map +0 -1
- package/dist/HubSpotConnector.d.ts +0 -523
- package/dist/HubSpotConnector.d.ts.map +0 -1
- package/dist/HubSpotConnector.js +0 -3110
- package/dist/HubSpotConnector.js.map +0 -1
- package/dist/IMISConnector.d.ts +0 -171
- package/dist/IMISConnector.d.ts.map +0 -1
- package/dist/IMISConnector.js +0 -695
- package/dist/IMISConnector.js.map +0 -1
- package/dist/MJToMJConnector.d.ts +0 -279
- package/dist/MJToMJConnector.d.ts.map +0 -1
- package/dist/MJToMJConnector.js +0 -893
- package/dist/MJToMJConnector.js.map +0 -1
- package/dist/MagnetMailConnector.d.ts +0 -163
- package/dist/MagnetMailConnector.d.ts.map +0 -1
- package/dist/MagnetMailConnector.js +0 -823
- package/dist/MagnetMailConnector.js.map +0 -1
- package/dist/MailchimpConnector.d.ts +0 -197
- package/dist/MailchimpConnector.d.ts.map +0 -1
- package/dist/MailchimpConnector.js +0 -791
- package/dist/MailchimpConnector.js.map +0 -1
- package/dist/MemberSuiteConnector.d.ts +0 -185
- package/dist/MemberSuiteConnector.d.ts.map +0 -1
- package/dist/MemberSuiteConnector.js +0 -762
- package/dist/MemberSuiteConnector.js.map +0 -1
- package/dist/NeonCRMConnector.d.ts +0 -273
- package/dist/NeonCRMConnector.d.ts.map +0 -1
- package/dist/NeonCRMConnector.js +0 -1039
- package/dist/NeonCRMConnector.js.map +0 -1
- package/dist/NetForumConnector.d.ts +0 -170
- package/dist/NetForumConnector.d.ts.map +0 -1
- package/dist/NetForumConnector.js +0 -778
- package/dist/NetForumConnector.js.map +0 -1
- package/dist/NetSuiteConnector.d.ts +0 -136
- package/dist/NetSuiteConnector.d.ts.map +0 -1
- package/dist/NetSuiteConnector.js +0 -703
- package/dist/NetSuiteConnector.js.map +0 -1
- package/dist/NimbleAMSConnector.d.ts +0 -142
- package/dist/NimbleAMSConnector.d.ts.map +0 -1
- package/dist/NimbleAMSConnector.js +0 -703
- package/dist/NimbleAMSConnector.js.map +0 -1
- package/dist/NoviConnector.d.ts +0 -178
- package/dist/NoviConnector.d.ts.map +0 -1
- package/dist/NoviConnector.js +0 -566
- package/dist/NoviConnector.js.map +0 -1
- package/dist/ORCIDConnector.d.ts +0 -207
- package/dist/ORCIDConnector.d.ts.map +0 -1
- package/dist/ORCIDConnector.js +0 -708
- package/dist/ORCIDConnector.js.map +0 -1
- package/dist/OpenWaterConnector.d.ts +0 -173
- package/dist/OpenWaterConnector.d.ts.map +0 -1
- package/dist/OpenWaterConnector.js +0 -893
- package/dist/OpenWaterConnector.js.map +0 -1
- package/dist/PathLMSConnector.d.ts +0 -395
- package/dist/PathLMSConnector.d.ts.map +0 -1
- package/dist/PathLMSConnector.js +0 -1281
- package/dist/PathLMSConnector.js.map +0 -1
- package/dist/PheedLoopConnector.d.ts +0 -119
- package/dist/PheedLoopConnector.d.ts.map +0 -1
- package/dist/PheedLoopConnector.js +0 -429
- package/dist/PheedLoopConnector.js.map +0 -1
- package/dist/PropFuelConnector.d.ts +0 -153
- package/dist/PropFuelConnector.d.ts.map +0 -1
- package/dist/PropFuelConnector.js +0 -450
- package/dist/PropFuelConnector.js.map +0 -1
- package/dist/QuickBooksConnector.d.ts +0 -92
- package/dist/QuickBooksConnector.d.ts.map +0 -1
- package/dist/QuickBooksConnector.js +0 -713
- package/dist/QuickBooksConnector.js.map +0 -1
- package/dist/RasaConnector.d.ts +0 -152
- package/dist/RasaConnector.d.ts.map +0 -1
- package/dist/RasaConnector.js +0 -929
- package/dist/RasaConnector.js.map +0 -1
- package/dist/Reach360Connector.d.ts +0 -175
- package/dist/Reach360Connector.d.ts.map +0 -1
- package/dist/Reach360Connector.js +0 -796
- package/dist/Reach360Connector.js.map +0 -1
- package/dist/RelationalDBConnector.d.ts +0 -108
- package/dist/RelationalDBConnector.d.ts.map +0 -1
- package/dist/RelationalDBConnector.js +0 -228
- package/dist/RelationalDBConnector.js.map +0 -1
- package/dist/RhythmConnector.d.ts +0 -188
- package/dist/RhythmConnector.d.ts.map +0 -1
- package/dist/RhythmConnector.js +0 -784
- package/dist/RhythmConnector.js.map +0 -1
- package/dist/SageIntacctConnector.d.ts +0 -337
- package/dist/SageIntacctConnector.d.ts.map +0 -1
- package/dist/SageIntacctConnector.js +0 -2081
- package/dist/SageIntacctConnector.js.map +0 -1
- package/dist/SalesforceConnector.d.ts +0 -461
- package/dist/SalesforceConnector.d.ts.map +0 -1
- package/dist/SalesforceConnector.js +0 -1862
- package/dist/SalesforceConnector.js.map +0 -1
- package/dist/SharePointConnector.d.ts +0 -188
- package/dist/SharePointConnector.d.ts.map +0 -1
- package/dist/SharePointConnector.js +0 -716
- package/dist/SharePointConnector.js.map +0 -1
- package/dist/WicketConnector.d.ts +0 -252
- package/dist/WicketConnector.d.ts.map +0 -1
- package/dist/WicketConnector.js +0 -938
- package/dist/WicketConnector.js.map +0 -1
- package/dist/WildApricotConnector.d.ts +0 -201
- package/dist/WildApricotConnector.d.ts.map +0 -1
- package/dist/WildApricotConnector.js +0 -1129
- package/dist/WildApricotConnector.js.map +0 -1
- package/dist/YourMembershipConnector.d.ts +0 -349
- package/dist/YourMembershipConnector.d.ts.map +0 -1
- package/dist/YourMembershipConnector.js +0 -4062
- package/dist/YourMembershipConnector.js.map +0 -1
- package/dist/generate-integration-actions.d.ts +0 -23
- package/dist/generate-integration-actions.d.ts.map +0 -1
- package/dist/generate-integration-actions.js +0 -201
- package/dist/generate-integration-actions.js.map +0 -1
package/dist/WicketConnector.js
DELETED
|
@@ -1,938 +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 { createHmac } from 'node:crypto';
|
|
8
|
-
import { RegisterClass } from '@memberjunction/global';
|
|
9
|
-
import { Metadata } from '@memberjunction/core';
|
|
10
|
-
import { BaseIntegrationConnector, BaseRESTIntegrationConnector, } from '@memberjunction/integration-engine';
|
|
11
|
-
// ─── Constants ───────────────────────────────────────────────────────
|
|
12
|
-
/** Default JWT expiration window in seconds (5 minutes) */
|
|
13
|
-
const JWT_EXPIRATION_SECONDS = 300;
|
|
14
|
-
/** Maximum retries for rate-limited or transient failures */
|
|
15
|
-
const MAX_RETRIES = 5;
|
|
16
|
-
/** HTTP request timeout in milliseconds */
|
|
17
|
-
const REQUEST_TIMEOUT_MS = 30_000;
|
|
18
|
-
/** Minimum milliseconds between API requests (Wicket: 500 req/min ≈ 120ms/req) */
|
|
19
|
-
const MIN_REQUEST_INTERVAL_MS = 125;
|
|
20
|
-
/** Default page size for Wicket API requests */
|
|
21
|
-
const DEFAULT_PAGE_SIZE = 100;
|
|
22
|
-
/**
|
|
23
|
-
* Maps integration object names to their JSON:API `type` values.
|
|
24
|
-
* Used when constructing JSON:API request bodies for CRUD operations.
|
|
25
|
-
*/
|
|
26
|
-
const WICKET_JSONAPI_TYPES = {
|
|
27
|
-
people: 'people',
|
|
28
|
-
organizations: 'organizations',
|
|
29
|
-
connections: 'connections',
|
|
30
|
-
groups: 'groups',
|
|
31
|
-
group_members: 'group_members',
|
|
32
|
-
person_memberships: 'person_memberships',
|
|
33
|
-
organization_memberships: 'organization_memberships',
|
|
34
|
-
memberships: 'memberships',
|
|
35
|
-
touchpoints: 'touchpoints',
|
|
36
|
-
emails: 'emails',
|
|
37
|
-
phones: 'phones',
|
|
38
|
-
addresses: 'addresses',
|
|
39
|
-
roles: 'roles',
|
|
40
|
-
user_identities: 'user_identities',
|
|
41
|
-
resource_tags: 'resource_tags',
|
|
42
|
-
people_emails: 'emails',
|
|
43
|
-
people_phones: 'phones',
|
|
44
|
-
people_addresses: 'addresses',
|
|
45
|
-
org_emails: 'emails',
|
|
46
|
-
org_phones: 'phones',
|
|
47
|
-
org_addresses: 'addresses',
|
|
48
|
-
};
|
|
49
|
-
/**
|
|
50
|
-
* Objects that support POST /query endpoints for advanced search.
|
|
51
|
-
* These accept nested filters, OR logic, and data_fields searching.
|
|
52
|
-
*/
|
|
53
|
-
const SEARCHABLE_OBJECTS = new Set([
|
|
54
|
-
'people',
|
|
55
|
-
'organizations',
|
|
56
|
-
'connections',
|
|
57
|
-
'groups',
|
|
58
|
-
'group_members',
|
|
59
|
-
'person_memberships',
|
|
60
|
-
'organization_memberships',
|
|
61
|
-
]);
|
|
62
|
-
/**
|
|
63
|
-
* Objects that are immutable (no update/delete allowed).
|
|
64
|
-
* Touchpoints are audit trail records by design.
|
|
65
|
-
*/
|
|
66
|
-
const IMMUTABLE_OBJECTS = new Set(['touchpoints']);
|
|
67
|
-
// ─── JWT Helper ──────────────────────────────────────────────────────
|
|
68
|
-
/**
|
|
69
|
-
* Encodes a buffer or string to base64url format (RFC 7515).
|
|
70
|
-
* Replaces `+` with `-`, `/` with `_`, and strips trailing `=` padding.
|
|
71
|
-
*/
|
|
72
|
-
function base64url(input) {
|
|
73
|
-
const b64 = Buffer.from(input, 'utf8').toString('base64');
|
|
74
|
-
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
75
|
-
}
|
|
76
|
-
/** Encodes a Buffer to base64url format. */
|
|
77
|
-
function base64urlBuffer(input) {
|
|
78
|
-
return input.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Generates a signed JWT token for Wicket API authentication.
|
|
82
|
-
* Uses HS256 (HMAC-SHA256) algorithm as required by the Wicket API.
|
|
83
|
-
*
|
|
84
|
-
* @param credentials - Wicket credentials containing API secret, admin UUID, and tenant
|
|
85
|
-
* @returns Signed JWT string
|
|
86
|
-
*/
|
|
87
|
-
/** Resolves the Wicket API base URL from credentials (direct URL or tenant-derived). */
|
|
88
|
-
function resolveWicketBaseURL(credentials) {
|
|
89
|
-
if (credentials.ApiUrl) {
|
|
90
|
-
// Strip trailing slash for consistency
|
|
91
|
-
return credentials.ApiUrl.replace(/\/+$/, '');
|
|
92
|
-
}
|
|
93
|
-
if (credentials.TenantName) {
|
|
94
|
-
return `https://${credentials.TenantName}-api.wicketcloud.com`;
|
|
95
|
-
}
|
|
96
|
-
throw new Error('Cannot determine Wicket API base URL. Provide apiUrl or tenantName in credentials.');
|
|
97
|
-
}
|
|
98
|
-
function generateWicketJWT(credentials) {
|
|
99
|
-
const now = Math.floor(Date.now() / 1000);
|
|
100
|
-
const apiUrl = resolveWicketBaseURL(credentials);
|
|
101
|
-
const header = { alg: 'HS256', typ: 'JWT' };
|
|
102
|
-
const payload = {
|
|
103
|
-
exp: now + JWT_EXPIRATION_SECONDS,
|
|
104
|
-
sub: credentials.AdminUserUUID,
|
|
105
|
-
aud: apiUrl,
|
|
106
|
-
};
|
|
107
|
-
if (credentials.IssuerDomain) {
|
|
108
|
-
payload['iss'] = credentials.IssuerDomain;
|
|
109
|
-
}
|
|
110
|
-
const encodedHeader = base64url(JSON.stringify(header));
|
|
111
|
-
const encodedPayload = base64url(JSON.stringify(payload));
|
|
112
|
-
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
113
|
-
const signature = createHmac('sha256', credentials.ApiSecret)
|
|
114
|
-
.update(signingInput)
|
|
115
|
-
.digest();
|
|
116
|
-
return `${signingInput}.${base64urlBuffer(signature)}`;
|
|
117
|
-
}
|
|
118
|
-
// ─── Connector ───────────────────────────────────────────────────────
|
|
119
|
-
/**
|
|
120
|
-
* Connector for the Wicket membership management platform via its JSON:API REST API.
|
|
121
|
-
*
|
|
122
|
-
* Extends BaseRESTIntegrationConnector to leverage metadata-driven object/field
|
|
123
|
-
* discovery, generic pagination handling, and template variable resolution.
|
|
124
|
-
*
|
|
125
|
-
* Supports:
|
|
126
|
-
* - JWT Bearer authentication (HS256, generated from API secret)
|
|
127
|
-
* - JSON:API response format normalization (flattens `data[].attributes`)
|
|
128
|
-
* - Page-number pagination (`page[number]`, `page[size]`)
|
|
129
|
-
* - Timestamp-based watermarks via `filter[updated_at_gteq]`
|
|
130
|
-
* - Full CRUD operations (bidirectional sync)
|
|
131
|
-
* - Advanced search via POST /query endpoints
|
|
132
|
-
* - Rate limiting (500 req/min, auto-throttle + retry on 429)
|
|
133
|
-
*
|
|
134
|
-
* @see https://wicketapi.docs.apiary.io/
|
|
135
|
-
*/
|
|
136
|
-
let WicketConnector = class WicketConnector extends BaseRESTIntegrationConnector {
|
|
137
|
-
constructor() {
|
|
138
|
-
super(...arguments);
|
|
139
|
-
/** Timestamp of the last API request, used for throttling */
|
|
140
|
-
this.lastRequestTime = 0;
|
|
141
|
-
/** Cached auth context to avoid regenerating JWT on every request */
|
|
142
|
-
this.cachedAuth = null;
|
|
143
|
-
/** Expiration time of the cached auth context */
|
|
144
|
-
this.cachedAuthExpiresAt = 0;
|
|
145
|
-
}
|
|
146
|
-
// ─── Identity ────────────────────────────────────────────────────
|
|
147
|
-
// Required for the three-way invariant (metadata.Name = connector
|
|
148
|
-
// IntegrationName = @RegisterClass tag). Base class default
|
|
149
|
-
// (this.constructor.name) would return 'WicketConnector' which would
|
|
150
|
-
// miss the metadata lookup for 'Wicket'.
|
|
151
|
-
get IntegrationName() { return 'Wicket'; }
|
|
152
|
-
// ─── Capability Getters ──────────────────────────────────────────
|
|
153
|
-
get SupportsCreate() { return true; }
|
|
154
|
-
get SupportsUpdate() { return true; }
|
|
155
|
-
get SupportsDelete() { return true; }
|
|
156
|
-
get SupportsSearch() { return true; }
|
|
157
|
-
// ─── BaseRESTIntegrationConnector abstract implementations ───────
|
|
158
|
-
async Authenticate(companyIntegration, contextUser) {
|
|
159
|
-
// Return cached auth if still valid (with 30s safety margin)
|
|
160
|
-
const now = Math.floor(Date.now() / 1000);
|
|
161
|
-
if (this.cachedAuth && this.cachedAuthExpiresAt > now + 30) {
|
|
162
|
-
return this.cachedAuth;
|
|
163
|
-
}
|
|
164
|
-
const credentials = await this.LoadCredentials(companyIntegration, contextUser);
|
|
165
|
-
const token = generateWicketJWT(credentials);
|
|
166
|
-
const baseURL = resolveWicketBaseURL(credentials);
|
|
167
|
-
const auth = {
|
|
168
|
-
Token: token,
|
|
169
|
-
Credentials: credentials,
|
|
170
|
-
BaseURL: baseURL,
|
|
171
|
-
};
|
|
172
|
-
this.cachedAuth = auth;
|
|
173
|
-
this.cachedAuthExpiresAt = now + JWT_EXPIRATION_SECONDS;
|
|
174
|
-
return auth;
|
|
175
|
-
}
|
|
176
|
-
BuildHeaders(auth) {
|
|
177
|
-
return {
|
|
178
|
-
'Authorization': `Bearer ${auth.Token}`,
|
|
179
|
-
'Accept': 'application/json',
|
|
180
|
-
'Content-Type': 'application/vnd.api+json',
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
async MakeHTTPRequest(_auth, url, method, headers, body) {
|
|
184
|
-
await this.ThrottleRequest();
|
|
185
|
-
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
186
|
-
const response = await this.FetchWithTimeout(url, method, headers, body);
|
|
187
|
-
this.lastRequestTime = Date.now();
|
|
188
|
-
if (response.status === 429) {
|
|
189
|
-
const delayMs = this.CalculateRetryDelay(response, attempt);
|
|
190
|
-
console.warn(`[Wicket] Rate limited (429), retrying in ${delayMs}ms ` +
|
|
191
|
-
`(attempt ${attempt + 1}/${MAX_RETRIES})`);
|
|
192
|
-
await this.Sleep(delayMs);
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
const responseBody = await this.ParseResponseBody(response);
|
|
196
|
-
return this.BuildRESTResponse(response, responseBody);
|
|
197
|
-
}
|
|
198
|
-
throw new Error(`Wicket API request failed after ${MAX_RETRIES} retries: ${url}`);
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Normalizes a JSON:API response into flat record objects.
|
|
202
|
-
*
|
|
203
|
-
* Wicket returns data in JSON:API format:
|
|
204
|
-
* ```json
|
|
205
|
-
* {
|
|
206
|
-
* "data": [{ "id": "uuid", "type": "people", "attributes": {...}, "relationships": {...} }],
|
|
207
|
-
* "included": [...],
|
|
208
|
-
* "meta": { "page": { "number": 1, "size": 25, "total_count": 500 } }
|
|
209
|
-
* }
|
|
210
|
-
* ```
|
|
211
|
-
*
|
|
212
|
-
* This method flattens each record to: `{ uuid: "...", given_name: "...", ... }`
|
|
213
|
-
*/
|
|
214
|
-
NormalizeResponse(rawBody, responseDataKey) {
|
|
215
|
-
const body = rawBody;
|
|
216
|
-
// Use responseDataKey if specified, otherwise default to "data"
|
|
217
|
-
const dataKey = responseDataKey ?? 'data';
|
|
218
|
-
const data = body[dataKey];
|
|
219
|
-
if (!data)
|
|
220
|
-
return [];
|
|
221
|
-
// Handle single-object responses (e.g., GET /people/{id})
|
|
222
|
-
if (!Array.isArray(data)) {
|
|
223
|
-
return [this.FlattenJsonApiRecord(data)];
|
|
224
|
-
}
|
|
225
|
-
return data.map(r => this.FlattenJsonApiRecord(r));
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Extracts pagination state from Wicket's JSON:API meta response.
|
|
229
|
-
*
|
|
230
|
-
* Wicket returns pagination info in:
|
|
231
|
-
* ```json
|
|
232
|
-
* { "meta": { "page": { "number": 1, "size": 25, "total_count": 500 } } }
|
|
233
|
-
* ```
|
|
234
|
-
*/
|
|
235
|
-
ExtractPaginationInfo(rawBody, _paginationType, currentPage, _currentOffset, pageSize) {
|
|
236
|
-
const body = rawBody;
|
|
237
|
-
const meta = body['meta'];
|
|
238
|
-
const pageInfo = meta?.['page'];
|
|
239
|
-
if (!pageInfo?.total_count) {
|
|
240
|
-
return { HasMore: false };
|
|
241
|
-
}
|
|
242
|
-
const totalPages = Math.ceil(pageInfo.total_count / (pageInfo.size ?? pageSize));
|
|
243
|
-
const hasMore = currentPage < totalPages;
|
|
244
|
-
return {
|
|
245
|
-
HasMore: hasMore,
|
|
246
|
-
NextPage: hasMore ? currentPage + 1 : undefined,
|
|
247
|
-
TotalRecords: pageInfo.total_count,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
GetBaseURL(companyIntegration) {
|
|
251
|
-
// If we have a cached auth context, use its base URL
|
|
252
|
-
if (this.cachedAuth) {
|
|
253
|
-
return this.cachedAuth.BaseURL;
|
|
254
|
-
}
|
|
255
|
-
// Fallback: parse from configuration JSON
|
|
256
|
-
const configJson = companyIntegration.Get('Configuration');
|
|
257
|
-
if (configJson) {
|
|
258
|
-
try {
|
|
259
|
-
const config = JSON.parse(configJson);
|
|
260
|
-
const apiUrl = config['apiUrl'] ?? config['ApiUrl'];
|
|
261
|
-
if (apiUrl)
|
|
262
|
-
return apiUrl.replace(/\/+$/, '');
|
|
263
|
-
const tenant = config['tenantName'] ?? config['TenantName'];
|
|
264
|
-
if (tenant)
|
|
265
|
-
return `https://${tenant}-api.wicketcloud.com`;
|
|
266
|
-
}
|
|
267
|
-
catch { /* fall through */ }
|
|
268
|
-
}
|
|
269
|
-
throw new Error('Cannot determine Wicket API base URL. Provide apiUrl or tenantName in credentials or configuration.');
|
|
270
|
-
}
|
|
271
|
-
// ─── Wicket-specific pagination ──────────────────────────────────
|
|
272
|
-
/**
|
|
273
|
-
* Overrides base pagination URL building to use Wicket's JSON:API parameter format.
|
|
274
|
-
* Wicket uses `page[number]` and `page[size]` (bracket notation).
|
|
275
|
-
*/
|
|
276
|
-
BuildPaginatedURL(basePath, obj, page, _offset, _cursor) {
|
|
277
|
-
const separator = basePath.includes('?') ? '&' : '?';
|
|
278
|
-
const pageSize = obj.DefaultPageSize || DEFAULT_PAGE_SIZE;
|
|
279
|
-
return `${basePath}${separator}page[number]=${page}&page[size]=${pageSize}`;
|
|
280
|
-
}
|
|
281
|
-
// ─── FetchChanges override for watermark support ─────────────────
|
|
282
|
-
/**
|
|
283
|
-
* Overrides FetchChanges to inject Wicket's timestamp-based watermark filter.
|
|
284
|
-
* Uses `filter[updated_at_gteq]` for incremental sync.
|
|
285
|
-
*/
|
|
286
|
-
async FetchChanges(ctx) {
|
|
287
|
-
// If watermark is provided, inject it as a filter parameter
|
|
288
|
-
if (ctx.WatermarkValue) {
|
|
289
|
-
const currentConfig = ctx.CompanyIntegration.Get('Configuration');
|
|
290
|
-
const config = currentConfig ? JSON.parse(currentConfig) : {};
|
|
291
|
-
// Store original and inject watermark filter into request
|
|
292
|
-
const originalWatermarkFilter = config['_watermarkFilter'];
|
|
293
|
-
config['_watermarkFilter'] = ctx.WatermarkValue;
|
|
294
|
-
// We'll use this in AppendDefaultQueryParams override behavior
|
|
295
|
-
}
|
|
296
|
-
const result = await super.FetchChanges(ctx);
|
|
297
|
-
// Set new watermark to current timestamp if we got records
|
|
298
|
-
if (result.Records.length > 0) {
|
|
299
|
-
result.NewWatermarkValue = new Date().toISOString();
|
|
300
|
-
}
|
|
301
|
-
return result;
|
|
302
|
-
}
|
|
303
|
-
// ─── DiscoverObjects ──────────────────────────────────────────────
|
|
304
|
-
/**
|
|
305
|
-
* Canonical Wicket top-level objects PLUS live-discovered per-tenant
|
|
306
|
-
* additional-info schemas (Wicket's "custom tables" concept — each
|
|
307
|
-
* org/event/etc can extend their data model via JSON-Schema-Forms).
|
|
308
|
-
*
|
|
309
|
-
* Standard objects are merged with what's already persisted so curated
|
|
310
|
-
* customizations aren't lost. The additional-info schemas live at
|
|
311
|
-
* `/additional_info_schemas` (Wicket exposes them as queryable
|
|
312
|
-
* resources); each schema becomes a Discovered IO.
|
|
313
|
-
*/
|
|
314
|
-
async DiscoverObjects(companyIntegration, contextUser) {
|
|
315
|
-
const persisted = await super.DiscoverObjects(companyIntegration, contextUser);
|
|
316
|
-
const canonical = [
|
|
317
|
-
{ Name: 'people', Label: 'People', Description: 'Member/person records. /people.', SupportsIncrementalSync: true, SupportsWrite: true },
|
|
318
|
-
{ Name: 'organizations', Label: 'Organizations', Description: 'Organization records. /organizations.', SupportsIncrementalSync: true, SupportsWrite: true },
|
|
319
|
-
{ Name: 'person_memberships', Label: 'Person Memberships', Description: 'Per-person membership assignments. /person_memberships.', SupportsIncrementalSync: true, SupportsWrite: true },
|
|
320
|
-
{ Name: 'organization_memberships', Label: 'Organization Memberships', Description: 'Per-organization membership assignments. /organization_memberships.', SupportsIncrementalSync: true, SupportsWrite: true },
|
|
321
|
-
{ Name: 'memberships', Label: 'Membership Definitions', Description: 'Tenant-defined membership types/categories. /memberships.', SupportsIncrementalSync: false, SupportsWrite: true },
|
|
322
|
-
{ Name: 'connections', Label: 'Connections', Description: 'Relationships between people / between people and organizations. /connections.', SupportsIncrementalSync: true, SupportsWrite: true },
|
|
323
|
-
{ Name: 'groups', Label: 'Groups', Description: 'Group records (committees, chapters, etc). /groups.', SupportsIncrementalSync: true, SupportsWrite: true },
|
|
324
|
-
{ Name: 'group_members', Label: 'Group Members', Description: 'Group membership entries. /group_members.', SupportsIncrementalSync: true, SupportsWrite: true },
|
|
325
|
-
{ Name: 'segments', Label: 'Segments', Description: 'Saved audience segments. /segments.', SupportsIncrementalSync: false, SupportsWrite: false },
|
|
326
|
-
{ Name: 'taxonomies', Label: 'Taxonomies', Description: 'Tenant-defined taxonomy categories. /taxonomies.', SupportsIncrementalSync: false, SupportsWrite: true },
|
|
327
|
-
{ Name: 'additional_info_schemas', Label: 'Additional Info Schemas', Description: 'Tenant-defined JSON-Schema extensions for custom attributes (per-tenant "custom tables"). /additional_info_schemas.', SupportsIncrementalSync: false, SupportsWrite: false },
|
|
328
|
-
];
|
|
329
|
-
const byName = new Map();
|
|
330
|
-
for (const o of persisted)
|
|
331
|
-
byName.set(o.Name.toLowerCase(), o);
|
|
332
|
-
for (const c of canonical) {
|
|
333
|
-
if (!byName.has(c.Name.toLowerCase()))
|
|
334
|
-
byName.set(c.Name.toLowerCase(), c);
|
|
335
|
-
}
|
|
336
|
-
// Live-discover per-tenant additional-info schemas — each one is a
|
|
337
|
-
// custom table the operator has defined. Best-effort: probe failure
|
|
338
|
-
// is non-fatal (canonical set still returned).
|
|
339
|
-
try {
|
|
340
|
-
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
341
|
-
const headers = this.BuildHeaders(auth);
|
|
342
|
-
const baseURL = this.GetBaseURL(companyIntegration);
|
|
343
|
-
const resp = await this.MakeHTTPRequest(auth, `${baseURL}/additional_info_schemas`, 'GET', headers);
|
|
344
|
-
if (resp.Status === 200) {
|
|
345
|
-
const body = resp.Body;
|
|
346
|
-
for (const sch of body.data ?? []) {
|
|
347
|
-
const slug = sch.attributes?.slug ?? sch.attributes?.name ?? sch.id;
|
|
348
|
-
if (!slug)
|
|
349
|
-
continue;
|
|
350
|
-
const ioName = `additional_info.${slug}`;
|
|
351
|
-
if (!byName.has(ioName.toLowerCase())) {
|
|
352
|
-
byName.set(ioName.toLowerCase(), {
|
|
353
|
-
Name: ioName,
|
|
354
|
-
Label: sch.attributes?.name ?? slug,
|
|
355
|
-
Description: sch.attributes?.description ?? `Tenant-defined additional-info schema (Wicket custom table). /additional_info_schemas/${sch.id}.`,
|
|
356
|
-
SupportsIncrementalSync: false,
|
|
357
|
-
SupportsWrite: false,
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
catch (err) {
|
|
364
|
-
console.warn(`[Wicket] additional_info_schemas live discovery failed (non-fatal, canonical set still returned): ${err instanceof Error ? err.message : err}`);
|
|
365
|
-
}
|
|
366
|
-
return [...byName.values()];
|
|
367
|
-
}
|
|
368
|
-
// ─── TestConnection ──────────────────────────────────────────────
|
|
369
|
-
/** Tests connectivity by authenticating and fetching 1 person record. */
|
|
370
|
-
async TestConnection(companyIntegration, contextUser) {
|
|
371
|
-
try {
|
|
372
|
-
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
373
|
-
const headers = this.BuildHeaders(auth);
|
|
374
|
-
const baseURL = this.GetBaseURL(companyIntegration);
|
|
375
|
-
const response = await this.MakeHTTPRequest(auth, `${baseURL}/people?page[size]=1`, 'GET', headers);
|
|
376
|
-
if (response.Status >= 200 && response.Status < 300) {
|
|
377
|
-
return {
|
|
378
|
-
Success: true,
|
|
379
|
-
Message: 'Successfully connected to Wicket API',
|
|
380
|
-
ServerVersion: 'Wicket JSON:API v1',
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
const bodyPreview = this.FormatBodyPreview(response.Body);
|
|
384
|
-
return {
|
|
385
|
-
Success: false,
|
|
386
|
-
Message: `Wicket API returned ${response.Status}: ${bodyPreview}`,
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
catch (err) {
|
|
390
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
391
|
-
return { Success: false, Message: `Connection failed: ${message}` };
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
// ─── GetDefaultFieldMappings ─────────────────────────────────────
|
|
395
|
-
GetDefaultFieldMappings(objectName, _entityName) {
|
|
396
|
-
switch (objectName) {
|
|
397
|
-
case 'people':
|
|
398
|
-
return this.GetPeopleFieldMappings();
|
|
399
|
-
case 'organizations':
|
|
400
|
-
return this.GetOrganizationFieldMappings();
|
|
401
|
-
case 'person_memberships':
|
|
402
|
-
return this.GetPersonMembershipFieldMappings();
|
|
403
|
-
case 'connections':
|
|
404
|
-
return this.GetConnectionFieldMappings();
|
|
405
|
-
default:
|
|
406
|
-
return [];
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
// ─── Default Configuration ───────────────────────────────────────
|
|
410
|
-
GetDefaultConfiguration() {
|
|
411
|
-
return {
|
|
412
|
-
DefaultSchemaName: 'Wicket',
|
|
413
|
-
DefaultObjects: [],
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
// ─── CRUD Operations (bidirectional sync support) ────────────────
|
|
417
|
-
/**
|
|
418
|
-
* Creates a new record in the Wicket API.
|
|
419
|
-
*
|
|
420
|
-
* @param companyIntegration - Company integration with credentials
|
|
421
|
-
* @param objectName - Wicket object type (e.g., "people", "organizations")
|
|
422
|
-
* @param attributes - Field values for the new record
|
|
423
|
-
* @param contextUser - User context for authorization
|
|
424
|
-
* @param relationships - Optional JSON:API relationships to include
|
|
425
|
-
* @returns CRUD result with the created record's external ID
|
|
426
|
-
*/
|
|
427
|
-
async CreateRecord(ctx) {
|
|
428
|
-
const companyIntegration = ctx.CompanyIntegration;
|
|
429
|
-
const objectName = ctx.ObjectName;
|
|
430
|
-
this.ValidateWriteAllowed(objectName);
|
|
431
|
-
const auth = await this.Authenticate(companyIntegration, ctx.ContextUser);
|
|
432
|
-
const headers = this.BuildHeaders(auth);
|
|
433
|
-
const baseURL = this.GetBaseURL(companyIntegration);
|
|
434
|
-
const apiPath = this.ResolveObjectAPIPath(objectName);
|
|
435
|
-
const body = this.BuildJsonApiBody(objectName, ctx.Attributes, ctx.Relationships);
|
|
436
|
-
try {
|
|
437
|
-
const response = await this.MakeHTTPRequest(auth, `${baseURL}${apiPath}`, 'POST', headers, body);
|
|
438
|
-
return this.ParseCRUDResponse(response, 'create', objectName);
|
|
439
|
-
}
|
|
440
|
-
catch (err) {
|
|
441
|
-
return this.BuildCRUDError(err);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* Updates an existing record in the Wicket API.
|
|
446
|
-
*
|
|
447
|
-
* @param companyIntegration - Company integration with credentials
|
|
448
|
-
* @param objectName - Wicket object type
|
|
449
|
-
* @param externalID - UUID of the record to update
|
|
450
|
-
* @param attributes - Field values to update
|
|
451
|
-
* @param contextUser - User context for authorization
|
|
452
|
-
* @param relationships - Optional JSON:API relationships to update
|
|
453
|
-
* @returns CRUD result indicating success or failure
|
|
454
|
-
*/
|
|
455
|
-
async UpdateRecord(ctx) {
|
|
456
|
-
const companyIntegration = ctx.CompanyIntegration;
|
|
457
|
-
const objectName = ctx.ObjectName;
|
|
458
|
-
this.ValidateWriteAllowed(objectName);
|
|
459
|
-
const auth = await this.Authenticate(companyIntegration, ctx.ContextUser);
|
|
460
|
-
const headers = this.BuildHeaders(auth);
|
|
461
|
-
const baseURL = this.GetBaseURL(companyIntegration);
|
|
462
|
-
const apiPath = this.ResolveObjectAPIPath(objectName);
|
|
463
|
-
const body = this.BuildJsonApiBody(objectName, ctx.Attributes, ctx.Relationships, ctx.ExternalID);
|
|
464
|
-
try {
|
|
465
|
-
const response = await this.MakeHTTPRequest(auth, `${baseURL}${apiPath}/${ctx.ExternalID}`, 'PATCH', headers, body);
|
|
466
|
-
return this.ParseCRUDResponse(response, 'update');
|
|
467
|
-
}
|
|
468
|
-
catch (err) {
|
|
469
|
-
return this.BuildCRUDError(err);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
|
-
* Deletes a record from the Wicket API.
|
|
474
|
-
*
|
|
475
|
-
* @param companyIntegration - Company integration with credentials
|
|
476
|
-
* @param objectName - Wicket object type
|
|
477
|
-
* @param externalID - UUID of the record to delete
|
|
478
|
-
* @param contextUser - User context for authorization
|
|
479
|
-
* @returns CRUD result indicating success or failure
|
|
480
|
-
*/
|
|
481
|
-
async DeleteRecord(ctx) {
|
|
482
|
-
const companyIntegration = ctx.CompanyIntegration;
|
|
483
|
-
const objectName = ctx.ObjectName;
|
|
484
|
-
this.ValidateWriteAllowed(objectName);
|
|
485
|
-
this.ValidateDeleteAllowed(objectName);
|
|
486
|
-
const auth = await this.Authenticate(companyIntegration, ctx.ContextUser);
|
|
487
|
-
const headers = this.BuildHeaders(auth);
|
|
488
|
-
const baseURL = this.GetBaseURL(companyIntegration);
|
|
489
|
-
const apiPath = this.ResolveObjectAPIPath(objectName);
|
|
490
|
-
try {
|
|
491
|
-
const response = await this.MakeHTTPRequest(auth, `${baseURL}${apiPath}/${ctx.ExternalID}`, 'DELETE', headers);
|
|
492
|
-
return {
|
|
493
|
-
Success: response.Status === 204 || (response.Status >= 200 && response.Status < 300),
|
|
494
|
-
ExternalID: ctx.ExternalID,
|
|
495
|
-
StatusCode: response.Status,
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
catch (err) {
|
|
499
|
-
return this.BuildCRUDError(err);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
/**
|
|
503
|
-
* Retrieves a single record from the Wicket API by ID.
|
|
504
|
-
*
|
|
505
|
-
* @param companyIntegration - Company integration with credentials
|
|
506
|
-
* @param objectName - Wicket object type
|
|
507
|
-
* @param externalID - UUID of the record to retrieve
|
|
508
|
-
* @param contextUser - User context for authorization
|
|
509
|
-
* @returns The record as an ExternalRecord, or null if not found
|
|
510
|
-
*/
|
|
511
|
-
async GetRecord(ctx) {
|
|
512
|
-
const companyIntegration = ctx.CompanyIntegration;
|
|
513
|
-
const objectName = ctx.ObjectName;
|
|
514
|
-
const auth = await this.Authenticate(companyIntegration, ctx.ContextUser);
|
|
515
|
-
const headers = this.BuildHeaders(auth);
|
|
516
|
-
const baseURL = this.GetBaseURL(companyIntegration);
|
|
517
|
-
const apiPath = this.ResolveObjectAPIPath(objectName);
|
|
518
|
-
try {
|
|
519
|
-
const response = await this.MakeHTTPRequest(auth, `${baseURL}${apiPath}/${ctx.ExternalID}`, 'GET', headers);
|
|
520
|
-
if (response.Status === 404)
|
|
521
|
-
return null;
|
|
522
|
-
if (response.Status < 200 || response.Status >= 300)
|
|
523
|
-
return null;
|
|
524
|
-
const records = this.NormalizeResponse(response.Body, 'data');
|
|
525
|
-
if (records.length === 0)
|
|
526
|
-
return null;
|
|
527
|
-
return {
|
|
528
|
-
ExternalID: ctx.ExternalID,
|
|
529
|
-
ObjectType: objectName,
|
|
530
|
-
Fields: records[0],
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
catch {
|
|
534
|
-
return null;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
// ─── Search Operations ───────────────────────────────────────────
|
|
538
|
-
/**
|
|
539
|
-
* Searches for records using Wicket's POST /query endpoints.
|
|
540
|
-
*
|
|
541
|
-
* Supports advanced filtering with predicates:
|
|
542
|
-
* - Equality: `given_name_eq`, `status_not_eq`
|
|
543
|
-
* - Comparison: `created_at_gteq`, `updated_at_lt`
|
|
544
|
-
* - Pattern: `email_cont`, `name_start`
|
|
545
|
-
* - Null checks: `phone_present`, `address_blank`
|
|
546
|
-
* - Array: `status_in` (comma-separated values)
|
|
547
|
-
*
|
|
548
|
-
* @param companyIntegration - Company integration with credentials
|
|
549
|
-
* @param objectName - Wicket object type (must be in SEARCHABLE_OBJECTS)
|
|
550
|
-
* @param options - Search filters, sorting, and pagination
|
|
551
|
-
* @param contextUser - User context for authorization
|
|
552
|
-
* @returns Search results with records, total count, and pagination info
|
|
553
|
-
*/
|
|
554
|
-
async SearchRecords(ctx) {
|
|
555
|
-
const companyIntegration = ctx.CompanyIntegration;
|
|
556
|
-
const objectName = ctx.ObjectName;
|
|
557
|
-
if (!SEARCHABLE_OBJECTS.has(objectName)) {
|
|
558
|
-
throw new Error(`Search is not supported for "${objectName}". ` +
|
|
559
|
-
`Searchable objects: ${[...SEARCHABLE_OBJECTS].join(', ')}`);
|
|
560
|
-
}
|
|
561
|
-
const auth = await this.Authenticate(companyIntegration, ctx.ContextUser);
|
|
562
|
-
const headers = this.BuildHeaders(auth);
|
|
563
|
-
const baseURL = this.GetBaseURL(companyIntegration);
|
|
564
|
-
const wicketOptions = {
|
|
565
|
-
Filters: ctx.Filters,
|
|
566
|
-
Sort: ctx.Sort,
|
|
567
|
-
Page: ctx.Page,
|
|
568
|
-
PageSize: ctx.PageSize,
|
|
569
|
-
};
|
|
570
|
-
const queryBody = this.BuildSearchQueryBody(wicketOptions);
|
|
571
|
-
const response = await this.MakeHTTPRequest(auth, `${baseURL}/${objectName}/query`, 'POST', headers, queryBody);
|
|
572
|
-
return this.ParseSearchResponse(response, objectName);
|
|
573
|
-
}
|
|
574
|
-
// ─── Default Field Mapping Builders ──────────────────────────────
|
|
575
|
-
GetPeopleFieldMappings() {
|
|
576
|
-
return [
|
|
577
|
-
{ SourceFieldName: 'uuid', DestinationFieldName: 'ExternalID', IsKeyField: true },
|
|
578
|
-
{ SourceFieldName: 'given_name', DestinationFieldName: 'FirstName' },
|
|
579
|
-
{ SourceFieldName: 'family_name', DestinationFieldName: 'LastName' },
|
|
580
|
-
{ SourceFieldName: 'full_name', DestinationFieldName: 'FullName' },
|
|
581
|
-
{ SourceFieldName: 'job_title', DestinationFieldName: 'JobTitle' },
|
|
582
|
-
{ SourceFieldName: 'gender', DestinationFieldName: 'Gender' },
|
|
583
|
-
{ SourceFieldName: 'birth_date', DestinationFieldName: 'BirthDate' },
|
|
584
|
-
{ SourceFieldName: 'language', DestinationFieldName: 'Language' },
|
|
585
|
-
{ SourceFieldName: 'membership_number', DestinationFieldName: 'MembershipNumber' },
|
|
586
|
-
];
|
|
587
|
-
}
|
|
588
|
-
GetOrganizationFieldMappings() {
|
|
589
|
-
return [
|
|
590
|
-
{ SourceFieldName: 'uuid', DestinationFieldName: 'ExternalID', IsKeyField: true },
|
|
591
|
-
{ SourceFieldName: 'legal_name', DestinationFieldName: 'Name' },
|
|
592
|
-
{ SourceFieldName: 'alternate_name', DestinationFieldName: 'AlternateName' },
|
|
593
|
-
{ SourceFieldName: 'description', DestinationFieldName: 'Description' },
|
|
594
|
-
{ SourceFieldName: 'identifying_number', DestinationFieldName: 'IdentifyingNumber' },
|
|
595
|
-
{ SourceFieldName: 'type', DestinationFieldName: 'OrganizationType' },
|
|
596
|
-
];
|
|
597
|
-
}
|
|
598
|
-
GetPersonMembershipFieldMappings() {
|
|
599
|
-
return [
|
|
600
|
-
{ SourceFieldName: 'uuid', DestinationFieldName: 'ExternalID', IsKeyField: true },
|
|
601
|
-
{ SourceFieldName: 'starts_at', DestinationFieldName: 'StartDate' },
|
|
602
|
-
{ SourceFieldName: 'ends_at', DestinationFieldName: 'EndDate' },
|
|
603
|
-
];
|
|
604
|
-
}
|
|
605
|
-
GetConnectionFieldMappings() {
|
|
606
|
-
return [
|
|
607
|
-
{ SourceFieldName: 'uuid', DestinationFieldName: 'ExternalID', IsKeyField: true },
|
|
608
|
-
{ SourceFieldName: 'type', DestinationFieldName: 'ConnectionType' },
|
|
609
|
-
];
|
|
610
|
-
}
|
|
611
|
-
// ─── Credential Management ───────────────────────────────────────
|
|
612
|
-
/**
|
|
613
|
-
* Reads Wicket credentials from the linked Credential entity,
|
|
614
|
-
* or falls back to CompanyIntegration Configuration JSON.
|
|
615
|
-
*/
|
|
616
|
-
async LoadCredentials(companyIntegration, contextUser) {
|
|
617
|
-
const credentialID = companyIntegration.Get('CredentialID');
|
|
618
|
-
if (credentialID) {
|
|
619
|
-
const creds = await this.LoadFromCredentialEntity(credentialID, contextUser);
|
|
620
|
-
if (creds)
|
|
621
|
-
return creds;
|
|
622
|
-
}
|
|
623
|
-
const configJson = companyIntegration.Get('Configuration');
|
|
624
|
-
if (configJson) {
|
|
625
|
-
const creds = this.ParseCredentialJson(configJson);
|
|
626
|
-
if (creds)
|
|
627
|
-
return creds;
|
|
628
|
-
}
|
|
629
|
-
throw new Error('No Wicket credentials found. Attach a credential with apiSecret, adminUserUUID, ' +
|
|
630
|
-
'and either apiUrl or tenantName. Or set Configuration JSON on the CompanyIntegration.');
|
|
631
|
-
}
|
|
632
|
-
/** Loads credentials from a Credential entity by ID. */
|
|
633
|
-
async LoadFromCredentialEntity(credentialID, contextUser, provider) {
|
|
634
|
-
const md = provider ?? new Metadata();
|
|
635
|
-
const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
|
|
636
|
-
const loaded = await credential.Load(credentialID);
|
|
637
|
-
if (!loaded || !credential.Values)
|
|
638
|
-
return null;
|
|
639
|
-
return this.ParseCredentialJson(credential.Values);
|
|
640
|
-
}
|
|
641
|
-
/** Parses a JSON string to extract Wicket credentials. Returns null if required fields missing. */
|
|
642
|
-
ParseCredentialJson(json) {
|
|
643
|
-
try {
|
|
644
|
-
const parsed = JSON.parse(json);
|
|
645
|
-
const apiSecret = parsed['apiSecret'] ?? parsed['ApiSecret'] ?? parsed['api_secret'];
|
|
646
|
-
const adminUUID = parsed['adminUserUUID'] ?? parsed['AdminUserUUID'] ?? parsed['admin_user_uuid'];
|
|
647
|
-
const tenant = parsed['tenantName'] ?? parsed['TenantName'] ?? parsed['tenant_name'] ?? null;
|
|
648
|
-
const apiUrl = parsed['apiUrl'] ?? parsed['ApiUrl'] ?? parsed['api_url'] ?? null;
|
|
649
|
-
if (!apiSecret || !adminUUID)
|
|
650
|
-
return null;
|
|
651
|
-
// Need either a direct API URL or a tenant name to derive the URL
|
|
652
|
-
if (!apiUrl && !tenant)
|
|
653
|
-
return null;
|
|
654
|
-
return {
|
|
655
|
-
ApiSecret: apiSecret,
|
|
656
|
-
AdminUserUUID: adminUUID,
|
|
657
|
-
TenantName: tenant,
|
|
658
|
-
ApiUrl: apiUrl,
|
|
659
|
-
IssuerDomain: parsed['issuerDomain'] ?? parsed['IssuerDomain'] ?? null,
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
catch {
|
|
663
|
-
return null;
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
// ─── JSON:API Body Builders ──────────────────────────────────────
|
|
667
|
-
/**
|
|
668
|
-
* Builds a JSON:API-compliant request body for create/update operations.
|
|
669
|
-
*
|
|
670
|
-
* Output format:
|
|
671
|
-
* ```json
|
|
672
|
-
* {
|
|
673
|
-
* "data": {
|
|
674
|
-
* "type": "people",
|
|
675
|
-
* "attributes": { "given_name": "Jane", ... },
|
|
676
|
-
* "relationships": { ... }
|
|
677
|
-
* }
|
|
678
|
-
* }
|
|
679
|
-
* ```
|
|
680
|
-
*/
|
|
681
|
-
BuildJsonApiBody(objectName, attributes, relationships, externalID) {
|
|
682
|
-
const jsonApiType = WICKET_JSONAPI_TYPES[objectName] ?? objectName;
|
|
683
|
-
const data = {
|
|
684
|
-
type: jsonApiType,
|
|
685
|
-
attributes: this.StripSystemFields(attributes),
|
|
686
|
-
};
|
|
687
|
-
if (externalID) {
|
|
688
|
-
data['id'] = externalID;
|
|
689
|
-
}
|
|
690
|
-
if (relationships && Object.keys(relationships).length > 0) {
|
|
691
|
-
data['relationships'] = relationships;
|
|
692
|
-
}
|
|
693
|
-
return { data };
|
|
694
|
-
}
|
|
695
|
-
/** Removes system-managed fields from attributes before sending to Wicket. */
|
|
696
|
-
StripSystemFields(attributes) {
|
|
697
|
-
const systemFields = new Set(['uuid', 'id', 'created_at', 'updated_at', 'slug']);
|
|
698
|
-
const cleaned = {};
|
|
699
|
-
for (const [key, value] of Object.entries(attributes)) {
|
|
700
|
-
if (!systemFields.has(key)) {
|
|
701
|
-
cleaned[key] = value;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
return cleaned;
|
|
705
|
-
}
|
|
706
|
-
/** Builds the POST body for a Wicket /query search endpoint. */
|
|
707
|
-
BuildSearchQueryBody(options) {
|
|
708
|
-
const body = {
|
|
709
|
-
filter: options.Filters,
|
|
710
|
-
page: {
|
|
711
|
-
number: options.Page ?? 1,
|
|
712
|
-
size: options.PageSize ?? DEFAULT_PAGE_SIZE,
|
|
713
|
-
},
|
|
714
|
-
};
|
|
715
|
-
if (options.Sort) {
|
|
716
|
-
body['sort'] = options.Sort;
|
|
717
|
-
}
|
|
718
|
-
return body;
|
|
719
|
-
}
|
|
720
|
-
// ─── Response Parsers ────────────────────────────────────────────
|
|
721
|
-
/**
|
|
722
|
-
* Flattens a JSON:API record from the nested format:
|
|
723
|
-
* ```json
|
|
724
|
-
* { "id": "uuid", "type": "people", "attributes": { "given_name": "John", ... }, "relationships": {...} }
|
|
725
|
-
* ```
|
|
726
|
-
* into a flat object with all attributes at the top level plus `uuid` and `type`.
|
|
727
|
-
*/
|
|
728
|
-
FlattenJsonApiRecord(record) {
|
|
729
|
-
const attributes = record['attributes'];
|
|
730
|
-
const result = {};
|
|
731
|
-
// Add flattened attributes first
|
|
732
|
-
if (attributes) {
|
|
733
|
-
for (const [key, value] of Object.entries(attributes)) {
|
|
734
|
-
result[key] = value;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
// Add system fields
|
|
738
|
-
result['uuid'] = record['id'];
|
|
739
|
-
result['type'] = record['type'];
|
|
740
|
-
// Extract relationship IDs for easy access
|
|
741
|
-
this.FlattenRelationships(record, result);
|
|
742
|
-
return result;
|
|
743
|
-
}
|
|
744
|
-
/**
|
|
745
|
-
* Extracts relationship IDs from JSON:API relationships and adds them as flat fields.
|
|
746
|
-
* E.g., `relationships.person.data.id` becomes `person_id` on the flat record.
|
|
747
|
-
*/
|
|
748
|
-
FlattenRelationships(record, target) {
|
|
749
|
-
const relationships = record['relationships'];
|
|
750
|
-
if (!relationships)
|
|
751
|
-
return;
|
|
752
|
-
for (const [relName, relData] of Object.entries(relationships)) {
|
|
753
|
-
const rel = relData;
|
|
754
|
-
if (!rel?.['data'])
|
|
755
|
-
continue;
|
|
756
|
-
const data = rel['data'];
|
|
757
|
-
if (Array.isArray(data)) {
|
|
758
|
-
// Has-many: store as array of IDs
|
|
759
|
-
target[`${relName}_ids`] = data.map(d => d['id']);
|
|
760
|
-
}
|
|
761
|
-
else {
|
|
762
|
-
// Belongs-to: store as single ID
|
|
763
|
-
const belongsTo = data;
|
|
764
|
-
target[`${relName}_id`] = belongsTo['id'];
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
/** Parses a CRUD operation response into a WicketCRUDResult. */
|
|
769
|
-
ParseCRUDResponse(response, operation, objectName) {
|
|
770
|
-
const success = response.Status >= 200 && response.Status < 300;
|
|
771
|
-
if (!success) {
|
|
772
|
-
const bodyPreview = this.FormatBodyPreview(response.Body);
|
|
773
|
-
return {
|
|
774
|
-
Success: false,
|
|
775
|
-
ErrorMessage: `Wicket ${operation} failed with status ${response.Status}: ${bodyPreview}`,
|
|
776
|
-
StatusCode: response.Status,
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
// Extract the created/updated record ID from the response
|
|
780
|
-
const body = response.Body;
|
|
781
|
-
const data = body['data'];
|
|
782
|
-
const externalID = data?.['id'];
|
|
783
|
-
// CREATE-ONLY: a 2xx create with no record ID is a silent record-loss bug (duplicate
|
|
784
|
-
// creates on the next sync). Fail loudly via the base helper. Update/Delete are unchanged —
|
|
785
|
-
// they legitimately may not echo an ID in the body.
|
|
786
|
-
if (operation === 'create') {
|
|
787
|
-
return this.BuildCreatedResult(externalID, response.Status, objectName ?? 'record');
|
|
788
|
-
}
|
|
789
|
-
return {
|
|
790
|
-
Success: true,
|
|
791
|
-
ExternalID: externalID,
|
|
792
|
-
StatusCode: response.Status,
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
/** Parses a search response into a WicketSearchResult. */
|
|
796
|
-
ParseSearchResponse(response, objectName) {
|
|
797
|
-
if (response.Status < 200 || response.Status >= 300) {
|
|
798
|
-
const bodyPreview = this.FormatBodyPreview(response.Body);
|
|
799
|
-
throw new Error(`Wicket search failed with status ${response.Status}: ${bodyPreview}`);
|
|
800
|
-
}
|
|
801
|
-
const body = response.Body;
|
|
802
|
-
const records = this.NormalizeResponse(body, 'data');
|
|
803
|
-
const meta = body['meta'];
|
|
804
|
-
const pageInfo = meta?.['page'];
|
|
805
|
-
const totalCount = pageInfo?.total_count ?? records.length;
|
|
806
|
-
const currentPage = pageInfo?.number ?? 1;
|
|
807
|
-
const pageSize = pageInfo?.size ?? DEFAULT_PAGE_SIZE;
|
|
808
|
-
const totalPages = Math.ceil(totalCount / pageSize);
|
|
809
|
-
return {
|
|
810
|
-
Records: records.map(r => ({
|
|
811
|
-
ExternalID: String(r['uuid'] ?? ''),
|
|
812
|
-
ObjectType: objectName,
|
|
813
|
-
Fields: r,
|
|
814
|
-
})),
|
|
815
|
-
TotalCount: totalCount,
|
|
816
|
-
HasMore: currentPage < totalPages,
|
|
817
|
-
};
|
|
818
|
-
}
|
|
819
|
-
// ─── Validation ──────────────────────────────────────────────────
|
|
820
|
-
/** Throws if the object does not support write operations. */
|
|
821
|
-
ValidateWriteAllowed(objectName) {
|
|
822
|
-
if (IMMUTABLE_OBJECTS.has(objectName)) {
|
|
823
|
-
throw new Error(`Write operations are not allowed on "${objectName}". ` +
|
|
824
|
-
`Touchpoints are immutable audit trail records.`);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
/** Throws if the object does not support delete operations. */
|
|
828
|
-
ValidateDeleteAllowed(objectName) {
|
|
829
|
-
// Touchpoints and resource_tags typically don't support delete
|
|
830
|
-
const nonDeletable = new Set(['touchpoints', 'resource_tags']);
|
|
831
|
-
if (nonDeletable.has(objectName)) {
|
|
832
|
-
throw new Error(`Delete operations are not supported for "${objectName}".`);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
// ─── URL Helpers ─────────────────────────────────────────────────
|
|
836
|
-
/**
|
|
837
|
-
* Resolves the API path for a given Wicket object name.
|
|
838
|
-
* Handles top-level and nested (contact info) objects.
|
|
839
|
-
*/
|
|
840
|
-
ResolveObjectAPIPath(objectName) {
|
|
841
|
-
// Contact info objects nested under people
|
|
842
|
-
if (objectName === 'people_emails')
|
|
843
|
-
return '/people';
|
|
844
|
-
if (objectName === 'people_phones')
|
|
845
|
-
return '/people';
|
|
846
|
-
if (objectName === 'people_addresses')
|
|
847
|
-
return '/people';
|
|
848
|
-
if (objectName === 'org_emails')
|
|
849
|
-
return '/organizations';
|
|
850
|
-
if (objectName === 'org_phones')
|
|
851
|
-
return '/organizations';
|
|
852
|
-
if (objectName === 'org_addresses')
|
|
853
|
-
return '/organizations';
|
|
854
|
-
// For direct CRUD, emails/phones/addresses use their direct endpoints
|
|
855
|
-
if (objectName === 'emails' || objectName === 'phones' || objectName === 'addresses') {
|
|
856
|
-
return `/${objectName}`;
|
|
857
|
-
}
|
|
858
|
-
// Top-level objects
|
|
859
|
-
return `/${objectName}`;
|
|
860
|
-
}
|
|
861
|
-
// ─── HTTP Helpers ────────────────────────────────────────────────
|
|
862
|
-
/** Ensures minimum interval between API requests to avoid rate limiting. */
|
|
863
|
-
async ThrottleRequest() {
|
|
864
|
-
const elapsed = Date.now() - this.lastRequestTime;
|
|
865
|
-
if (elapsed < MIN_REQUEST_INTERVAL_MS) {
|
|
866
|
-
await this.Sleep(MIN_REQUEST_INTERVAL_MS - elapsed);
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
/** Executes an HTTP request with a timeout and optional body. */
|
|
870
|
-
async FetchWithTimeout(url, method, headers, body) {
|
|
871
|
-
const controller = new AbortController();
|
|
872
|
-
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
873
|
-
const fetchOptions = {
|
|
874
|
-
method,
|
|
875
|
-
headers,
|
|
876
|
-
signal: controller.signal,
|
|
877
|
-
};
|
|
878
|
-
if (body && (method === 'POST' || method === 'PATCH' || method === 'PUT')) {
|
|
879
|
-
fetchOptions.body = JSON.stringify(body);
|
|
880
|
-
}
|
|
881
|
-
try {
|
|
882
|
-
return await fetch(url, fetchOptions);
|
|
883
|
-
}
|
|
884
|
-
catch (err) {
|
|
885
|
-
if (err instanceof Error && err.name === 'AbortError') {
|
|
886
|
-
throw new Error(`Wicket API request timed out after ${REQUEST_TIMEOUT_MS / 1000}s: ${url}`);
|
|
887
|
-
}
|
|
888
|
-
throw err;
|
|
889
|
-
}
|
|
890
|
-
finally {
|
|
891
|
-
clearTimeout(timeoutId);
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
/** Safely parses a response body as JSON, falling back to text. */
|
|
895
|
-
async ParseResponseBody(response) {
|
|
896
|
-
const contentType = response.headers.get('content-type') ?? '';
|
|
897
|
-
if (contentType.includes('json')) {
|
|
898
|
-
return response.json();
|
|
899
|
-
}
|
|
900
|
-
return response.text();
|
|
901
|
-
}
|
|
902
|
-
/** Calculates retry delay from Retry-After header or exponential backoff. */
|
|
903
|
-
CalculateRetryDelay(response, attempt) {
|
|
904
|
-
const retryAfter = response.headers.get('retry-after');
|
|
905
|
-
if (retryAfter) {
|
|
906
|
-
const parsed = parseInt(retryAfter, 10);
|
|
907
|
-
if (!isNaN(parsed) && parsed > 0)
|
|
908
|
-
return parsed * 1000;
|
|
909
|
-
}
|
|
910
|
-
return Math.min(1000 * Math.pow(2, attempt), 30000);
|
|
911
|
-
}
|
|
912
|
-
/** Converts a fetch Response + parsed body into a RESTResponse. */
|
|
913
|
-
BuildRESTResponse(response, body) {
|
|
914
|
-
const headers = {};
|
|
915
|
-
response.headers.forEach((v, k) => { headers[k.toLowerCase()] = v; });
|
|
916
|
-
return { Status: response.status, Body: body, Headers: headers };
|
|
917
|
-
}
|
|
918
|
-
/** Formats a response body as a truncated string for error messages. */
|
|
919
|
-
FormatBodyPreview(body) {
|
|
920
|
-
if (typeof body === 'string')
|
|
921
|
-
return body.slice(0, 500);
|
|
922
|
-
return JSON.stringify(body).slice(0, 500);
|
|
923
|
-
}
|
|
924
|
-
/** Builds a WicketCRUDResult from a caught error. */
|
|
925
|
-
BuildCRUDError(err) {
|
|
926
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
927
|
-
return { Success: false, ErrorMessage: message, StatusCode: 0 };
|
|
928
|
-
}
|
|
929
|
-
/** Returns a promise that resolves after the specified number of milliseconds. */
|
|
930
|
-
Sleep(ms) {
|
|
931
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
932
|
-
}
|
|
933
|
-
};
|
|
934
|
-
WicketConnector = __decorate([
|
|
935
|
-
RegisterClass(BaseIntegrationConnector, 'WicketConnector')
|
|
936
|
-
], WicketConnector);
|
|
937
|
-
export { WicketConnector };
|
|
938
|
-
//# sourceMappingURL=WicketConnector.js.map
|