@memberjunction/connector-wild-apricot 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/WildApricotConnector.d.ts +190 -0
- package/dist/WildApricotConnector.js +620 -0
- package/dist/WildApricotConnector.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { type UserInfo } from '@memberjunction/core';
|
|
2
|
+
import type { MJCompanyIntegrationEntity, MJIntegrationObjectEntity } from '@memberjunction/core-entities';
|
|
3
|
+
import { BaseRESTIntegrationConnector, type RESTAuthContext, type RESTResponse, type PaginationState, type PaginationType, type ConnectionTestResult, type FetchContext, type FetchBatchResult, type RateLimitPolicy, type SourceSchemaInfo } from '@memberjunction/integration-engine';
|
|
4
|
+
/**
|
|
5
|
+
* Wild Apricot membership-management connector (Admin API v2.3).
|
|
6
|
+
*
|
|
7
|
+
* ── AUTH ──────────────────────────────────────────────────────────────────
|
|
8
|
+
* OAuth 2.0 `client_credentials`. The admin API Key is sent as the HTTP-Basic
|
|
9
|
+
* USERNAME (empty password) on `POST https://oauth.wildapricot.org/auth/token`
|
|
10
|
+
* with `grant_type=client_credentials&scope=auto`. Both the Basic token-endpoint
|
|
11
|
+
* header AND the resulting `Authorization: Bearer <access_token>` header are built
|
|
12
|
+
* via the shared auth-helpers ({@link OAuth2TokenManager} with `UseBasicAuth:true`)
|
|
13
|
+
* — no inline base64/crypto lives in this connector. The access token is cached +
|
|
14
|
+
* auto-refreshed by the manager; the `client_credentials` grant returns no refresh
|
|
15
|
+
* token, so expiry re-mints via the same API key (per Configuration.TokenRefreshStrategy).
|
|
16
|
+
*
|
|
17
|
+
* ── TENANT ANCHOR (accountId) ───────────────────────────────────────────────
|
|
18
|
+
* Every data path is `/accounts/{accountId}/…`. The accountId is a per-tenant anchor,
|
|
19
|
+
* NOT a synced parent record: {@link TestConnection} / {@link Authenticate} resolve it
|
|
20
|
+
* by issuing `GET /v2.3/accounts` (no id in path) and taking the first account's Id when
|
|
21
|
+
* the credential omits AccountId (Configuration.accountIdDiscovery). It is cached on the
|
|
22
|
+
* auth context and substituted into `{accountId}` in {@link MakeHTTPRequest}. It is
|
|
23
|
+
* per-tenant config — NEVER hardcoded.
|
|
24
|
+
*
|
|
25
|
+
* ── DISCOVERY (mechanism only — NO baked catalog) ───────────────────────────
|
|
26
|
+
* Wild Apricot's object/field universe is credential-free-documented (public OpenAPI
|
|
27
|
+
* 9.14.0), so it is seeded as Declared metadata in the integration file. This connector
|
|
28
|
+
* therefore carries NO `WILD_APRICOT_OBJECTS` catalog constant (the deprecated
|
|
29
|
+
* connector's anti-pattern): {@link DiscoverObjects}/{@link DiscoverFields} inherit the
|
|
30
|
+
* base cache-driven implementation that reads the Declared metadata. That is the
|
|
31
|
+
* sanctioned "case-1 → Declared metadata" mechanism.
|
|
32
|
+
*
|
|
33
|
+
* ── CRUD ────────────────────────────────────────────────────────────────────
|
|
34
|
+
* Generic per-operation CRUD from {@link BaseRESTIntegrationConnector} (reads
|
|
35
|
+
* Create/Update/Delete APIPath/Method/BodyShape/IDLocation off each IO row) is used
|
|
36
|
+
* as-is; create fails LOUDLY on an empty response ID via `BuildCreatedResult`. No CRUD
|
|
37
|
+
* verb is re-implemented here.
|
|
38
|
+
*
|
|
39
|
+
* ── PAGINATION ──────────────────────────────────────────────────────────────
|
|
40
|
+
* Offset pagination via Wild Apricot's `$top`/`$skip` params (NOT the base's
|
|
41
|
+
* `limit`/`offset`), clamped to a 100-item max page per spec — {@link BuildPaginatedURL}
|
|
42
|
+
* + {@link ExtractPaginationInfo} are overridden for the vendor param names.
|
|
43
|
+
*
|
|
44
|
+
* ── THE ONE IDIOSYNCRATIC OVERRIDE: async Contacts list ─────────────────────
|
|
45
|
+
* `GET /accounts/{accountId}/contacts` defaults to ASYNC: it returns a `ResultId`
|
|
46
|
+
* that must be polled (`?resultId=<ResultId>`) until `State=Complete`, then the same
|
|
47
|
+
* URL returns the `Contacts` array. {@link FetchChanges} overrides ONLY the `Contact`
|
|
48
|
+
* object to run that request→poll→collect flow (bounded poll timeout); every other
|
|
49
|
+
* object delegates to the base flat/nested paginated fetch. See {@link FetchContacts}.
|
|
50
|
+
*/
|
|
51
|
+
export declare class WildApricotConnector extends BaseRESTIntegrationConnector {
|
|
52
|
+
/** Cached OAuth2 token manager (one per connector instance; the manager caches + refreshes the token). */
|
|
53
|
+
private tokenManager;
|
|
54
|
+
/** Cached tenant anchor accountId, resolved once per instance (per-tenant, never hardcoded). */
|
|
55
|
+
private cachedAccountId;
|
|
56
|
+
/** Verbatim three-way invariant name: IntegrationName getter === MJ: Integrations.Name. */
|
|
57
|
+
get IntegrationName(): string;
|
|
58
|
+
/**
|
|
59
|
+
* IntrospectSchema — pure WIRING of MJ's existing sampler into the declared catalog (the connector
|
|
60
|
+
* sample-union standard; see CONNECTOR_DISCOVERY_STANDARD.md). This connector adds NO discovery,
|
|
61
|
+
* merge, or sync logic — it only wires `DiscoverFieldsViaFetch` (MJ's sampler) into IntrospectSchema.
|
|
62
|
+
*
|
|
63
|
+
* `super.IntrospectSchema` yields the cache-driven Declared catalog (no measured widths). For each
|
|
64
|
+
* object we then call MJ's `DiscoverFieldsViaFetch` — MJ's own read-path sampler that measures real
|
|
65
|
+
* field widths and surfaces custom columns — and the shared PURE `mergeDeclaredWithSampledFields`
|
|
66
|
+
* unions the two by field name (adopt MJ's measured width; append MJ-discovered custom columns). MJ
|
|
67
|
+
* owns everything else (measurement, type/PK inference, persistence, reconcile, sync).
|
|
68
|
+
*
|
|
69
|
+
* Recursion note: `DiscoverFieldsViaFetch` falls back to the UNCHANGED `DiscoverFields` (cache-driven)
|
|
70
|
+
* when the read path can't run — never back into THIS method — so there is no infinite recursion.
|
|
71
|
+
* This connector does NOT override `DiscoverFields` to call any ViaFetch/ViaStream.
|
|
72
|
+
*
|
|
73
|
+
* Robustness: objects are sampled IN PARALLEL under a small bounded pool; any per-object failure
|
|
74
|
+
* keeps that object's declared fields, so a single bad sample never breaks introspection.
|
|
75
|
+
*/
|
|
76
|
+
IntrospectSchema(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<SourceSchemaInfo>;
|
|
77
|
+
get SupportsCreate(): boolean;
|
|
78
|
+
get SupportsUpdate(): boolean;
|
|
79
|
+
get SupportsDelete(): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Documented rate-limit policy (Configuration.RateLimitDetail). The general ceiling is
|
|
82
|
+
* 400 requests/min (≈6.67/s) for "other request types"; the two Contacts-specific
|
|
83
|
+
* ceilings (list=40/min, by-id=120/min) are lower, so the engine's AIMD bucket starts
|
|
84
|
+
* from the CONSERVATIVE general rate and backs off further on a 429 (honored via
|
|
85
|
+
* ExtractRetryAfterMs). Burst kept modest to respect the per-minute windows.
|
|
86
|
+
*/
|
|
87
|
+
get RateLimitPolicy(): RateLimitPolicy | null;
|
|
88
|
+
/** Parses Wild Apricot's 429 Retry-After (seconds) into ms so the AIMD bucket waits the full window. */
|
|
89
|
+
ExtractRetryAfterMs(error: unknown): number | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* Mints/refreshes the bearer token via the shared OAuth2 manager, then resolves the
|
|
92
|
+
* tenant accountId (from the credential config, else auto-discovered via GET /accounts).
|
|
93
|
+
* Returns the bearer token + accountId on the auth context.
|
|
94
|
+
*/
|
|
95
|
+
protected Authenticate(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<WildApricotAuthContext>;
|
|
96
|
+
/** Bearer header for API calls, built from the manager-minted token. No inline crypto. */
|
|
97
|
+
protected BuildHeaders(auth: WildApricotAuthContext): Record<string, string>;
|
|
98
|
+
/** Base URL: host + versioned path segment (e.g. https://api.wildapricot.org/v2.3). */
|
|
99
|
+
protected GetBaseURL(_companyIntegration: MJCompanyIntegrationEntity, auth: WildApricotAuthContext): string;
|
|
100
|
+
/**
|
|
101
|
+
* Executes an HTTP request via fetch. Substitutes the resolved `{accountId}` tenant anchor
|
|
102
|
+
* into the URL (the base leaves it as a template var; here it becomes the concrete tenant id)
|
|
103
|
+
* and parses JSON responses. The concrete connector owns the transport seam so tests override it.
|
|
104
|
+
*/
|
|
105
|
+
protected MakeHTTPRequest(auth: WildApricotAuthContext, url: string, method: string, headers: Record<string, string>, body?: unknown): Promise<RESTResponse>;
|
|
106
|
+
/**
|
|
107
|
+
* Extracts the record array from a Wild Apricot response. ResponseDataKey is null in the
|
|
108
|
+
* Declared metadata because the wrapper key varies by endpoint (e.g. `Contacts`, `Events`,
|
|
109
|
+
* `Invoices`) and some endpoints return a bare array. So: honor an explicit key when set,
|
|
110
|
+
* else return a root-level array, else unwrap the first array-valued property of an object,
|
|
111
|
+
* else wrap a single object. This handles both wrapped-collection and bare-array shapes.
|
|
112
|
+
*/
|
|
113
|
+
protected NormalizeResponse(rawBody: unknown, responseDataKey: string | null): Record<string, unknown>[];
|
|
114
|
+
/**
|
|
115
|
+
* Wild Apricot uses OData-style `$top`/`$skip` Offset pagination (NOT the base's `limit`/`offset`).
|
|
116
|
+
* `$top` is clamped to 100 per spec ("more than 100 → maximum 100 items returned"). Overridden
|
|
117
|
+
* here so the vendor param names + the 100 clamp are honored.
|
|
118
|
+
*/
|
|
119
|
+
protected BuildPaginatedURL(basePath: string, obj: MJIntegrationObjectEntity, _page: number, offset: number, _cursor?: string, effectivePageSize?: number): string;
|
|
120
|
+
/**
|
|
121
|
+
* Offset pagination termination: Wild Apricot list endpoints return fewer than `$top` items on
|
|
122
|
+
* the final page (and none past the end). More pages remain only when a FULL page came back.
|
|
123
|
+
*/
|
|
124
|
+
protected ExtractPaginationInfo(rawBody: unknown, _paginationType: PaginationType, _currentPage: number, currentOffset: number, pageSize: number): PaginationState;
|
|
125
|
+
/**
|
|
126
|
+
* Tests connectivity by minting a token and listing accounts (GET /accounts). When the credential
|
|
127
|
+
* omits AccountId, the first account's Id is adopted as the tenant anchor (Configuration.accountIdDiscovery).
|
|
128
|
+
*/
|
|
129
|
+
TestConnection(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<ConnectionTestResult>;
|
|
130
|
+
/**
|
|
131
|
+
* Fetches records. Only the `Contact` object is idiosyncratic — its list endpoint is ASYNC:
|
|
132
|
+
* the request returns a `ResultId` which must be polled until `State=Complete`. All other
|
|
133
|
+
* objects use the generic base flat/nested paginated fetch unchanged.
|
|
134
|
+
*/
|
|
135
|
+
FetchChanges(ctx: FetchContext): Promise<FetchBatchResult>;
|
|
136
|
+
/**
|
|
137
|
+
* Contacts async list flow (the single documented idiosyncrasy):
|
|
138
|
+
* 1. GET /accounts/{accountId}/contacts?$async=true&$skip&$top → 200/202 with a ResultId.
|
|
139
|
+
* 2. Poll GET /accounts/{accountId}/contacts?resultId=<ResultId> until State='Complete'
|
|
140
|
+
* (or the poll budget below is exhausted), then read the Contacts array.
|
|
141
|
+
* Incremental narrowing: when a watermark is present it is applied as a `$filter` on the
|
|
142
|
+
* documented cursor field (ProfileLastUpdated). Bounded by POLL_MAX_ATTEMPTS × POLL_INTERVAL_MS
|
|
143
|
+
* so a stuck async job can never hang the sync. Watermark persists on full-page success only.
|
|
144
|
+
*/
|
|
145
|
+
private FetchContacts;
|
|
146
|
+
/**
|
|
147
|
+
* Resolves the async Contacts response: if a ResultId is present, polls the same endpoint with
|
|
148
|
+
* `?resultId=<id>` until State='Complete' (bounded), then returns the Contacts array. If the
|
|
149
|
+
* initial response already carried the Contacts array synchronously, returns it directly.
|
|
150
|
+
*/
|
|
151
|
+
private ResolveAsyncContacts;
|
|
152
|
+
/** Substitutes the resolved `{accountId}` tenant anchor into a URL (case-insensitive on the var name). */
|
|
153
|
+
private SubstituteAccountId;
|
|
154
|
+
/**
|
|
155
|
+
* Resolves the tenant accountId: prefers the credential-configured value, else issues
|
|
156
|
+
* GET /accounts and adopts the first account's Id. Cached per instance. NEVER hardcoded.
|
|
157
|
+
*/
|
|
158
|
+
private ResolveAccountId;
|
|
159
|
+
/** Finds the first array-valued property of an object (a wrapped collection under a vendor key). */
|
|
160
|
+
private FindArrayInObject;
|
|
161
|
+
/** Reads the async-result State ('Complete'/'Processing'/'Failed'/…) from a poll body, tolerant of shape. */
|
|
162
|
+
private ReadState;
|
|
163
|
+
/** Builds a Contact record identity from its declared PK (falls back to a common id key, then a content hash). */
|
|
164
|
+
private BuildContactIdentity;
|
|
165
|
+
/** Max watermark value across a record batch (ISO8601 string comparison), never below the current one. */
|
|
166
|
+
private MaxWatermark;
|
|
167
|
+
/** Throws a descriptive error on a non-2xx contacts response (202 Accepted is treated as OK for the async kick-off). */
|
|
168
|
+
private AssertContactsOK;
|
|
169
|
+
/** Reads headers off an error-like object (for Retry-After parsing). */
|
|
170
|
+
private ExtractErrorHeaders;
|
|
171
|
+
/** Whether an error indicates a 429 rate-limit. */
|
|
172
|
+
private IsRateLimitError;
|
|
173
|
+
/**
|
|
174
|
+
* Resolves the API key + tenant config from the linked Credential entity, falling back to the
|
|
175
|
+
* CompanyIntegration.Configuration / APIKey. The credential bytes never leave this scope.
|
|
176
|
+
*/
|
|
177
|
+
private LoadCredentials;
|
|
178
|
+
/** Loads credential fields from a Credential entity's Values JSON. */
|
|
179
|
+
private LoadFromCredentialEntity;
|
|
180
|
+
/** Parses a JSON string into credential/config fields (tolerant of casing/aliases). */
|
|
181
|
+
private ParseConfigJson;
|
|
182
|
+
}
|
|
183
|
+
/** Auth context: resolved bearer token + tenant accountId + non-secret host/version overrides. */
|
|
184
|
+
interface WildApricotAuthContext extends RESTAuthContext {
|
|
185
|
+
Token: string;
|
|
186
|
+
AccountId: string;
|
|
187
|
+
BaseHost?: string;
|
|
188
|
+
ApiVersion?: string;
|
|
189
|
+
}
|
|
190
|
+
export {};
|
|
@@ -0,0 +1,620 @@
|
|
|
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, OAuth2TokenManager, } from '@memberjunction/integration-engine';
|
|
10
|
+
import { mergeDeclaredWithSampledFields } from '@memberjunction/connector-schema-merge';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
/**
|
|
13
|
+
* Wild Apricot membership-management connector (Admin API v2.3).
|
|
14
|
+
*
|
|
15
|
+
* ── AUTH ──────────────────────────────────────────────────────────────────
|
|
16
|
+
* OAuth 2.0 `client_credentials`. The admin API Key is sent as the HTTP-Basic
|
|
17
|
+
* USERNAME (empty password) on `POST https://oauth.wildapricot.org/auth/token`
|
|
18
|
+
* with `grant_type=client_credentials&scope=auto`. Both the Basic token-endpoint
|
|
19
|
+
* header AND the resulting `Authorization: Bearer <access_token>` header are built
|
|
20
|
+
* via the shared auth-helpers ({@link OAuth2TokenManager} with `UseBasicAuth:true`)
|
|
21
|
+
* — no inline base64/crypto lives in this connector. The access token is cached +
|
|
22
|
+
* auto-refreshed by the manager; the `client_credentials` grant returns no refresh
|
|
23
|
+
* token, so expiry re-mints via the same API key (per Configuration.TokenRefreshStrategy).
|
|
24
|
+
*
|
|
25
|
+
* ── TENANT ANCHOR (accountId) ───────────────────────────────────────────────
|
|
26
|
+
* Every data path is `/accounts/{accountId}/…`. The accountId is a per-tenant anchor,
|
|
27
|
+
* NOT a synced parent record: {@link TestConnection} / {@link Authenticate} resolve it
|
|
28
|
+
* by issuing `GET /v2.3/accounts` (no id in path) and taking the first account's Id when
|
|
29
|
+
* the credential omits AccountId (Configuration.accountIdDiscovery). It is cached on the
|
|
30
|
+
* auth context and substituted into `{accountId}` in {@link MakeHTTPRequest}. It is
|
|
31
|
+
* per-tenant config — NEVER hardcoded.
|
|
32
|
+
*
|
|
33
|
+
* ── DISCOVERY (mechanism only — NO baked catalog) ───────────────────────────
|
|
34
|
+
* Wild Apricot's object/field universe is credential-free-documented (public OpenAPI
|
|
35
|
+
* 9.14.0), so it is seeded as Declared metadata in the integration file. This connector
|
|
36
|
+
* therefore carries NO `WILD_APRICOT_OBJECTS` catalog constant (the deprecated
|
|
37
|
+
* connector's anti-pattern): {@link DiscoverObjects}/{@link DiscoverFields} inherit the
|
|
38
|
+
* base cache-driven implementation that reads the Declared metadata. That is the
|
|
39
|
+
* sanctioned "case-1 → Declared metadata" mechanism.
|
|
40
|
+
*
|
|
41
|
+
* ── CRUD ────────────────────────────────────────────────────────────────────
|
|
42
|
+
* Generic per-operation CRUD from {@link BaseRESTIntegrationConnector} (reads
|
|
43
|
+
* Create/Update/Delete APIPath/Method/BodyShape/IDLocation off each IO row) is used
|
|
44
|
+
* as-is; create fails LOUDLY on an empty response ID via `BuildCreatedResult`. No CRUD
|
|
45
|
+
* verb is re-implemented here.
|
|
46
|
+
*
|
|
47
|
+
* ── PAGINATION ──────────────────────────────────────────────────────────────
|
|
48
|
+
* Offset pagination via Wild Apricot's `$top`/`$skip` params (NOT the base's
|
|
49
|
+
* `limit`/`offset`), clamped to a 100-item max page per spec — {@link BuildPaginatedURL}
|
|
50
|
+
* + {@link ExtractPaginationInfo} are overridden for the vendor param names.
|
|
51
|
+
*
|
|
52
|
+
* ── THE ONE IDIOSYNCRATIC OVERRIDE: async Contacts list ─────────────────────
|
|
53
|
+
* `GET /accounts/{accountId}/contacts` defaults to ASYNC: it returns a `ResultId`
|
|
54
|
+
* that must be polled (`?resultId=<ResultId>`) until `State=Complete`, then the same
|
|
55
|
+
* URL returns the `Contacts` array. {@link FetchChanges} overrides ONLY the `Contact`
|
|
56
|
+
* object to run that request→poll→collect flow (bounded poll timeout); every other
|
|
57
|
+
* object delegates to the base flat/nested paginated fetch. See {@link FetchContacts}.
|
|
58
|
+
*/
|
|
59
|
+
let WildApricotConnector = class WildApricotConnector extends BaseRESTIntegrationConnector {
|
|
60
|
+
constructor() {
|
|
61
|
+
super(...arguments);
|
|
62
|
+
/** Cached OAuth2 token manager (one per connector instance; the manager caches + refreshes the token). */
|
|
63
|
+
this.tokenManager = new OAuth2TokenManager();
|
|
64
|
+
/** Cached tenant anchor accountId, resolved once per instance (per-tenant, never hardcoded). */
|
|
65
|
+
this.cachedAccountId = null;
|
|
66
|
+
}
|
|
67
|
+
/** Verbatim three-way invariant name: IntegrationName getter === MJ: Integrations.Name. */
|
|
68
|
+
get IntegrationName() {
|
|
69
|
+
return 'Wild Apricot';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* IntrospectSchema — pure WIRING of MJ's existing sampler into the declared catalog (the connector
|
|
73
|
+
* sample-union standard; see CONNECTOR_DISCOVERY_STANDARD.md). This connector adds NO discovery,
|
|
74
|
+
* merge, or sync logic — it only wires `DiscoverFieldsViaFetch` (MJ's sampler) into IntrospectSchema.
|
|
75
|
+
*
|
|
76
|
+
* `super.IntrospectSchema` yields the cache-driven Declared catalog (no measured widths). For each
|
|
77
|
+
* object we then call MJ's `DiscoverFieldsViaFetch` — MJ's own read-path sampler that measures real
|
|
78
|
+
* field widths and surfaces custom columns — and the shared PURE `mergeDeclaredWithSampledFields`
|
|
79
|
+
* unions the two by field name (adopt MJ's measured width; append MJ-discovered custom columns). MJ
|
|
80
|
+
* owns everything else (measurement, type/PK inference, persistence, reconcile, sync).
|
|
81
|
+
*
|
|
82
|
+
* Recursion note: `DiscoverFieldsViaFetch` falls back to the UNCHANGED `DiscoverFields` (cache-driven)
|
|
83
|
+
* when the read path can't run — never back into THIS method — so there is no infinite recursion.
|
|
84
|
+
* This connector does NOT override `DiscoverFields` to call any ViaFetch/ViaStream.
|
|
85
|
+
*
|
|
86
|
+
* Robustness: objects are sampled IN PARALLEL under a small bounded pool; any per-object failure
|
|
87
|
+
* keeps that object's declared fields, so a single bad sample never breaks introspection.
|
|
88
|
+
*/
|
|
89
|
+
async IntrospectSchema(companyIntegration, contextUser) {
|
|
90
|
+
const schema = await super.IntrospectSchema(companyIntegration, contextUser);
|
|
91
|
+
await runBounded(schema.Objects, 8, async (obj) => {
|
|
92
|
+
try {
|
|
93
|
+
const sampled = await this.DiscoverFieldsViaFetch(companyIntegration, obj.ExternalName, contextUser);
|
|
94
|
+
obj.Fields = mergeDeclaredWithSampledFields(obj.Fields, sampled);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Keep this object's declared fields — sampling is best-effort and never breaks introspection.
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return schema;
|
|
101
|
+
}
|
|
102
|
+
// ─── Capability surface ──────────────────────────────────────────────────
|
|
103
|
+
// Wild Apricot supports create/update/delete on many objects; the ACTUAL per-verb
|
|
104
|
+
// support is metadata-driven (each IO's Create/Update/Delete columns), and the base's
|
|
105
|
+
// generic CRUD throws for any verb whose columns are null. These getters declare the
|
|
106
|
+
// connector is capable so the engine offers the write surface.
|
|
107
|
+
get SupportsCreate() { return true; }
|
|
108
|
+
get SupportsUpdate() { return true; }
|
|
109
|
+
get SupportsDelete() { return true; }
|
|
110
|
+
/**
|
|
111
|
+
* Documented rate-limit policy (Configuration.RateLimitDetail). The general ceiling is
|
|
112
|
+
* 400 requests/min (≈6.67/s) for "other request types"; the two Contacts-specific
|
|
113
|
+
* ceilings (list=40/min, by-id=120/min) are lower, so the engine's AIMD bucket starts
|
|
114
|
+
* from the CONSERVATIVE general rate and backs off further on a 429 (honored via
|
|
115
|
+
* ExtractRetryAfterMs). Burst kept modest to respect the per-minute windows.
|
|
116
|
+
*/
|
|
117
|
+
get RateLimitPolicy() {
|
|
118
|
+
return { TokensPerSec: 6, Burst: 6, ThrottleBackoffFactor: 0.5 };
|
|
119
|
+
}
|
|
120
|
+
/** Parses Wild Apricot's 429 Retry-After (seconds) into ms so the AIMD bucket waits the full window. */
|
|
121
|
+
ExtractRetryAfterMs(error) {
|
|
122
|
+
// Wild Apricot returns HTTP 429 "wait for a minute"; when a Retry-After header is present we honor it.
|
|
123
|
+
const headers = this.ExtractErrorHeaders(error);
|
|
124
|
+
const retryAfter = headers?.['retry-after'];
|
|
125
|
+
if (retryAfter) {
|
|
126
|
+
const secs = Number(retryAfter);
|
|
127
|
+
if (Number.isFinite(secs) && secs >= 0)
|
|
128
|
+
return Math.round(secs * 1000);
|
|
129
|
+
}
|
|
130
|
+
// No header → the documented guidance is "wait for a minute" on a 429.
|
|
131
|
+
if (this.IsRateLimitError(error))
|
|
132
|
+
return 60_000;
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
// ─── Auth + transport (BaseRESTIntegrationConnector abstracts) ────────────
|
|
136
|
+
/**
|
|
137
|
+
* Mints/refreshes the bearer token via the shared OAuth2 manager, then resolves the
|
|
138
|
+
* tenant accountId (from the credential config, else auto-discovered via GET /accounts).
|
|
139
|
+
* Returns the bearer token + accountId on the auth context.
|
|
140
|
+
*/
|
|
141
|
+
async Authenticate(companyIntegration, contextUser) {
|
|
142
|
+
const creds = await this.LoadCredentials(companyIntegration, contextUser);
|
|
143
|
+
const token = await this.tokenManager.GetAccessToken({
|
|
144
|
+
TokenURL: creds.TokenUrl,
|
|
145
|
+
ClientId: creds.ApiKey, // API key is the Basic-auth USERNAME …
|
|
146
|
+
ClientSecret: '', // … with an EMPTY password.
|
|
147
|
+
Scopes: 'auto',
|
|
148
|
+
UseBasicAuth: true, // → Authorization: Basic base64(apiKey:) — built by the helper, no inline crypto.
|
|
149
|
+
}, 'client_credentials');
|
|
150
|
+
const accountId = await this.ResolveAccountId(creds, token.AccessToken);
|
|
151
|
+
return {
|
|
152
|
+
Token: token.AccessToken,
|
|
153
|
+
AccountId: accountId,
|
|
154
|
+
BaseHost: creds.BaseHost,
|
|
155
|
+
ApiVersion: creds.ApiVersion,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** Bearer header for API calls, built from the manager-minted token. No inline crypto. */
|
|
159
|
+
BuildHeaders(auth) {
|
|
160
|
+
return {
|
|
161
|
+
'Authorization': `Bearer ${auth.Token}`,
|
|
162
|
+
'Accept': 'application/json',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** Base URL: host + versioned path segment (e.g. https://api.wildapricot.org/v2.3). */
|
|
166
|
+
GetBaseURL(_companyIntegration, auth) {
|
|
167
|
+
const host = auth.BaseHost ?? WILDAPRICOT_API_HOST;
|
|
168
|
+
const version = auth.ApiVersion ?? DEFAULT_API_VERSION;
|
|
169
|
+
return `${host.replace(/\/+$/, '')}/${version}`;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Executes an HTTP request via fetch. Substitutes the resolved `{accountId}` tenant anchor
|
|
173
|
+
* into the URL (the base leaves it as a template var; here it becomes the concrete tenant id)
|
|
174
|
+
* and parses JSON responses. The concrete connector owns the transport seam so tests override it.
|
|
175
|
+
*/
|
|
176
|
+
async MakeHTTPRequest(auth, url, method, headers, body) {
|
|
177
|
+
const resolvedUrl = this.SubstituteAccountId(url, auth.AccountId);
|
|
178
|
+
const init = { method, headers };
|
|
179
|
+
if (body !== undefined && method !== 'GET' && method !== 'HEAD') {
|
|
180
|
+
init.body = typeof body === 'string' ? body : JSON.stringify(body);
|
|
181
|
+
init.headers['Content-Type'] = 'application/json';
|
|
182
|
+
}
|
|
183
|
+
const response = await fetch(resolvedUrl, init);
|
|
184
|
+
const responseHeaders = {};
|
|
185
|
+
response.headers.forEach((value, key) => { responseHeaders[key.toLowerCase()] = value; });
|
|
186
|
+
const text = await response.text();
|
|
187
|
+
let parsed = text;
|
|
188
|
+
const contentType = responseHeaders['content-type'] ?? '';
|
|
189
|
+
if (contentType.includes('json') || (text.length > 0 && (text[0] === '{' || text[0] === '['))) {
|
|
190
|
+
try {
|
|
191
|
+
parsed = JSON.parse(text);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
parsed = text;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { Status: response.status, Body: parsed, Headers: responseHeaders };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Extracts the record array from a Wild Apricot response. ResponseDataKey is null in the
|
|
201
|
+
* Declared metadata because the wrapper key varies by endpoint (e.g. `Contacts`, `Events`,
|
|
202
|
+
* `Invoices`) and some endpoints return a bare array. So: honor an explicit key when set,
|
|
203
|
+
* else return a root-level array, else unwrap the first array-valued property of an object,
|
|
204
|
+
* else wrap a single object. This handles both wrapped-collection and bare-array shapes.
|
|
205
|
+
*/
|
|
206
|
+
NormalizeResponse(rawBody, responseDataKey) {
|
|
207
|
+
if (responseDataKey && isRecord(rawBody)) {
|
|
208
|
+
const inner = rawBody[responseDataKey];
|
|
209
|
+
if (Array.isArray(inner))
|
|
210
|
+
return inner.filter(isRecord);
|
|
211
|
+
}
|
|
212
|
+
if (Array.isArray(rawBody))
|
|
213
|
+
return rawBody.filter(isRecord);
|
|
214
|
+
if (isRecord(rawBody)) {
|
|
215
|
+
const arr = this.FindArrayInObject(rawBody);
|
|
216
|
+
if (arr.length > 0)
|
|
217
|
+
return arr.filter(isRecord);
|
|
218
|
+
return [rawBody];
|
|
219
|
+
}
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Wild Apricot uses OData-style `$top`/`$skip` Offset pagination (NOT the base's `limit`/`offset`).
|
|
224
|
+
* `$top` is clamped to 100 per spec ("more than 100 → maximum 100 items returned"). Overridden
|
|
225
|
+
* here so the vendor param names + the 100 clamp are honored.
|
|
226
|
+
*/
|
|
227
|
+
BuildPaginatedURL(basePath, obj, _page, offset, _cursor, effectivePageSize) {
|
|
228
|
+
const pageSize = Math.min(effectivePageSize ?? obj.DefaultPageSize ?? WILDAPRICOT_MAX_PAGE_SIZE, WILDAPRICOT_MAX_PAGE_SIZE);
|
|
229
|
+
const separator = basePath.includes('?') ? '&' : '?';
|
|
230
|
+
return `${basePath}${separator}$skip=${offset}&$top=${pageSize}`;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Offset pagination termination: Wild Apricot list endpoints return fewer than `$top` items on
|
|
234
|
+
* the final page (and none past the end). More pages remain only when a FULL page came back.
|
|
235
|
+
*/
|
|
236
|
+
ExtractPaginationInfo(rawBody, _paginationType, _currentPage, currentOffset, pageSize) {
|
|
237
|
+
const records = this.NormalizeResponse(rawBody, null);
|
|
238
|
+
const count = records.length;
|
|
239
|
+
const hasMore = pageSize > 0 && count >= pageSize;
|
|
240
|
+
return {
|
|
241
|
+
HasMore: hasMore,
|
|
242
|
+
NextOffset: currentOffset + count,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
// ─── TestConnection (auto-discovers the accountId) ────────────────────────
|
|
246
|
+
/**
|
|
247
|
+
* Tests connectivity by minting a token and listing accounts (GET /accounts). When the credential
|
|
248
|
+
* omits AccountId, the first account's Id is adopted as the tenant anchor (Configuration.accountIdDiscovery).
|
|
249
|
+
*/
|
|
250
|
+
async TestConnection(companyIntegration, contextUser) {
|
|
251
|
+
try {
|
|
252
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
253
|
+
const url = `${this.GetBaseURL(companyIntegration, auth)}/accounts`;
|
|
254
|
+
const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
|
|
255
|
+
if (response.Status === 401 || response.Status === 403) {
|
|
256
|
+
return { Success: false, Message: `Wild Apricot authentication failed (HTTP ${response.Status}) — check the API key.` };
|
|
257
|
+
}
|
|
258
|
+
if (response.Status < 200 || response.Status >= 300) {
|
|
259
|
+
return { Success: false, Message: `Wild Apricot /accounts returned HTTP ${response.Status}.` };
|
|
260
|
+
}
|
|
261
|
+
const accounts = this.NormalizeResponse(response.Body, null);
|
|
262
|
+
return {
|
|
263
|
+
Success: true,
|
|
264
|
+
Message: `Connected to Wild Apricot account ${auth.AccountId}; ${accounts.length} account(s) accessible to this API key.`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
269
|
+
return { Success: false, Message: `Wild Apricot connection error: ${message}` };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// ─── THE idiosyncratic override: async Contacts list ──────────────────────
|
|
273
|
+
/**
|
|
274
|
+
* Fetches records. Only the `Contact` object is idiosyncratic — its list endpoint is ASYNC:
|
|
275
|
+
* the request returns a `ResultId` which must be polled until `State=Complete`. All other
|
|
276
|
+
* objects use the generic base flat/nested paginated fetch unchanged.
|
|
277
|
+
*/
|
|
278
|
+
async FetchChanges(ctx) {
|
|
279
|
+
if (ctx.ObjectName === CONTACT_OBJECT_NAME) {
|
|
280
|
+
return this.FetchContacts(ctx);
|
|
281
|
+
}
|
|
282
|
+
return super.FetchChanges(ctx);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Contacts async list flow (the single documented idiosyncrasy):
|
|
286
|
+
* 1. GET /accounts/{accountId}/contacts?$async=true&$skip&$top → 200/202 with a ResultId.
|
|
287
|
+
* 2. Poll GET /accounts/{accountId}/contacts?resultId=<ResultId> until State='Complete'
|
|
288
|
+
* (or the poll budget below is exhausted), then read the Contacts array.
|
|
289
|
+
* Incremental narrowing: when a watermark is present it is applied as a `$filter` on the
|
|
290
|
+
* documented cursor field (ProfileLastUpdated). Bounded by POLL_MAX_ATTEMPTS × POLL_INTERVAL_MS
|
|
291
|
+
* so a stuck async job can never hang the sync. Watermark persists on full-page success only.
|
|
292
|
+
*/
|
|
293
|
+
async FetchContacts(ctx) {
|
|
294
|
+
const auth = await this.Authenticate(ctx.CompanyIntegration, ctx.ContextUser);
|
|
295
|
+
const obj = this.GetCachedObject(ctx.CompanyIntegration.IntegrationID, ctx.ObjectName);
|
|
296
|
+
const fields = this.GetCachedFields(obj.ID);
|
|
297
|
+
const pkFieldNames = fields.filter(f => f.IsPrimaryKey).map(f => f.Name);
|
|
298
|
+
const effectivePk = pkFieldNames.length > 0 ? pkFieldNames : ['Id'];
|
|
299
|
+
const offset = ctx.CurrentOffset ?? 0;
|
|
300
|
+
const pageSize = Math.min(ctx.BatchSize && ctx.BatchSize > 0 ? ctx.BatchSize : WILDAPRICOT_MAX_PAGE_SIZE, WILDAPRICOT_MAX_PAGE_SIZE);
|
|
301
|
+
const base = `${this.GetBaseURL(ctx.CompanyIntegration, auth)}${obj.APIPath}`;
|
|
302
|
+
// Build the OData query string with LITERAL $-prefixed param names (not URLSearchParams, which
|
|
303
|
+
// percent-encodes `$` → `%24`), keeping it consistent with BuildPaginatedURL's $skip/$top.
|
|
304
|
+
const queryParts = [`$async=true`, `$skip=${offset}`, `$top=${pageSize}`];
|
|
305
|
+
if (ctx.WatermarkValue && obj.IncrementalWatermarkField) {
|
|
306
|
+
// Documented incremental strategy: $filter on ProfileLastUpdated (ISO8601 comparison).
|
|
307
|
+
queryParts.push(`$filter=${encodeURIComponent(`${obj.IncrementalWatermarkField} ge ${ctx.WatermarkValue}`)}`);
|
|
308
|
+
}
|
|
309
|
+
const requestUrl = `${base}?${queryParts.join('&')}`;
|
|
310
|
+
// Step 1: kick off the async query.
|
|
311
|
+
const start = await this.MakeHTTPRequest(auth, requestUrl, 'GET', this.BuildHeaders(auth));
|
|
312
|
+
this.AssertContactsOK(start, 'start async contacts query');
|
|
313
|
+
const records = await this.ResolveAsyncContacts(auth, base, start.Body);
|
|
314
|
+
// Step 2: emit with FULL-RECORD pass-through (Fields = raw source record).
|
|
315
|
+
const out = records.map(raw => ({
|
|
316
|
+
ExternalID: this.BuildContactIdentity(raw, effectivePk),
|
|
317
|
+
ObjectType: ctx.ObjectName,
|
|
318
|
+
Fields: raw,
|
|
319
|
+
}));
|
|
320
|
+
const hasMore = records.length >= pageSize;
|
|
321
|
+
const result = {
|
|
322
|
+
Records: out,
|
|
323
|
+
HasMore: hasMore,
|
|
324
|
+
NextOffset: offset + records.length,
|
|
325
|
+
};
|
|
326
|
+
// Persist the max watermark seen on this (full-page-success) batch.
|
|
327
|
+
if (obj.IncrementalWatermarkField) {
|
|
328
|
+
const maxWatermark = this.MaxWatermark(records, obj.IncrementalWatermarkField, ctx.WatermarkValue);
|
|
329
|
+
if (maxWatermark)
|
|
330
|
+
result.NewWatermarkValue = maxWatermark;
|
|
331
|
+
}
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Resolves the async Contacts response: if a ResultId is present, polls the same endpoint with
|
|
336
|
+
* `?resultId=<id>` until State='Complete' (bounded), then returns the Contacts array. If the
|
|
337
|
+
* initial response already carried the Contacts array synchronously, returns it directly.
|
|
338
|
+
*/
|
|
339
|
+
async ResolveAsyncContacts(auth, baseUrl, firstBody) {
|
|
340
|
+
const parsed = AsyncContactsSchema.safeParse(firstBody);
|
|
341
|
+
const resultId = parsed.success ? parsed.data.ResultId : undefined;
|
|
342
|
+
// Synchronous shape: the body already carries the Contacts array (no polling needed).
|
|
343
|
+
if (!resultId) {
|
|
344
|
+
return this.NormalizeResponse(firstBody, 'Contacts');
|
|
345
|
+
}
|
|
346
|
+
const pollUrl = `${baseUrl}?resultId=${encodeURIComponent(resultId)}`;
|
|
347
|
+
for (let attempt = 0; attempt < POLL_MAX_ATTEMPTS; attempt++) {
|
|
348
|
+
const poll = await this.MakeHTTPRequest(auth, pollUrl, 'GET', this.BuildHeaders(auth));
|
|
349
|
+
this.AssertContactsOK(poll, 'poll async contacts result');
|
|
350
|
+
const state = this.ReadState(poll.Body);
|
|
351
|
+
if (state === 'Complete') {
|
|
352
|
+
return this.NormalizeResponse(poll.Body, 'Contacts');
|
|
353
|
+
}
|
|
354
|
+
if (state === 'Failed') {
|
|
355
|
+
throw new Error(`Wild Apricot async contacts query failed (ResultId ${resultId}).`);
|
|
356
|
+
}
|
|
357
|
+
await delay(POLL_INTERVAL_MS);
|
|
358
|
+
}
|
|
359
|
+
throw new Error(`Wild Apricot async contacts query did not complete within ${(POLL_MAX_ATTEMPTS * POLL_INTERVAL_MS) / 1000}s ` +
|
|
360
|
+
`(ResultId ${resultId}) — poll timeout.`);
|
|
361
|
+
}
|
|
362
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
363
|
+
/** Substitutes the resolved `{accountId}` tenant anchor into a URL (case-insensitive on the var name). */
|
|
364
|
+
SubstituteAccountId(url, accountId) {
|
|
365
|
+
return url.replace(/\{accountId\}/gi, encodeURIComponent(accountId));
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Resolves the tenant accountId: prefers the credential-configured value, else issues
|
|
369
|
+
* GET /accounts and adopts the first account's Id. Cached per instance. NEVER hardcoded.
|
|
370
|
+
*/
|
|
371
|
+
async ResolveAccountId(creds, token) {
|
|
372
|
+
if (creds.AccountId)
|
|
373
|
+
return creds.AccountId;
|
|
374
|
+
if (this.cachedAccountId)
|
|
375
|
+
return this.cachedAccountId;
|
|
376
|
+
const host = creds.BaseHost ?? WILDAPRICOT_API_HOST;
|
|
377
|
+
const version = creds.ApiVersion ?? DEFAULT_API_VERSION;
|
|
378
|
+
const url = `${host.replace(/\/+$/, '')}/${version}/accounts`;
|
|
379
|
+
// Auth is not yet fully assembled (that's what we're resolving), so build a minimal context.
|
|
380
|
+
const bootstrapAuth = { Token: token, AccountId: '', BaseHost: creds.BaseHost, ApiVersion: creds.ApiVersion };
|
|
381
|
+
const response = await this.MakeHTTPRequest(bootstrapAuth, url, 'GET', this.BuildHeaders(bootstrapAuth));
|
|
382
|
+
if (response.Status < 200 || response.Status >= 300) {
|
|
383
|
+
throw new Error(`Wild Apricot account auto-discovery failed: GET /accounts returned HTTP ${response.Status}.`);
|
|
384
|
+
}
|
|
385
|
+
const accounts = this.NormalizeResponse(response.Body, null);
|
|
386
|
+
const first = accounts[0];
|
|
387
|
+
const id = first ? first['Id'] ?? first['id'] : undefined;
|
|
388
|
+
if (id == null) {
|
|
389
|
+
throw new Error('Wild Apricot account auto-discovery returned no accounts for this API key.');
|
|
390
|
+
}
|
|
391
|
+
this.cachedAccountId = String(id);
|
|
392
|
+
return this.cachedAccountId;
|
|
393
|
+
}
|
|
394
|
+
/** Finds the first array-valued property of an object (a wrapped collection under a vendor key). */
|
|
395
|
+
FindArrayInObject(obj) {
|
|
396
|
+
for (const v of Object.values(obj)) {
|
|
397
|
+
if (Array.isArray(v))
|
|
398
|
+
return v;
|
|
399
|
+
}
|
|
400
|
+
return [];
|
|
401
|
+
}
|
|
402
|
+
/** Reads the async-result State ('Complete'/'Processing'/'Failed'/…) from a poll body, tolerant of shape. */
|
|
403
|
+
ReadState(body) {
|
|
404
|
+
if (!isRecord(body))
|
|
405
|
+
return undefined;
|
|
406
|
+
const state = body['State'] ?? body['state'];
|
|
407
|
+
return typeof state === 'string' ? state : undefined;
|
|
408
|
+
}
|
|
409
|
+
/** Builds a Contact record identity from its declared PK (falls back to a common id key, then a content hash). */
|
|
410
|
+
BuildContactIdentity(raw, pkFieldNames) {
|
|
411
|
+
const parts = pkFieldNames.map(name => raw[name]).filter(v => v != null && String(v).length > 0);
|
|
412
|
+
if (parts.length === pkFieldNames.length && parts.length > 0) {
|
|
413
|
+
return parts.map(v => String(v)).join('|');
|
|
414
|
+
}
|
|
415
|
+
for (const k of ['Id', 'id', 'ID']) {
|
|
416
|
+
const v = raw[k];
|
|
417
|
+
if (v != null && String(v).length > 0)
|
|
418
|
+
return String(v);
|
|
419
|
+
}
|
|
420
|
+
return stableHash(raw);
|
|
421
|
+
}
|
|
422
|
+
/** Max watermark value across a record batch (ISO8601 string comparison), never below the current one. */
|
|
423
|
+
MaxWatermark(records, field, current) {
|
|
424
|
+
let max = current ?? '';
|
|
425
|
+
for (const r of records) {
|
|
426
|
+
const v = r[field];
|
|
427
|
+
if (typeof v === 'string' && v > max)
|
|
428
|
+
max = v;
|
|
429
|
+
}
|
|
430
|
+
return max.length > 0 && max !== (current ?? '') ? max : undefined;
|
|
431
|
+
}
|
|
432
|
+
/** Throws a descriptive error on a non-2xx contacts response (202 Accepted is treated as OK for the async kick-off). */
|
|
433
|
+
AssertContactsOK(response, action) {
|
|
434
|
+
if (response.Status === 202)
|
|
435
|
+
return;
|
|
436
|
+
if (response.Status < 200 || response.Status >= 300) {
|
|
437
|
+
throw new Error(`Wild Apricot failed to ${action}: HTTP ${response.Status}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/** Reads headers off an error-like object (for Retry-After parsing). */
|
|
441
|
+
ExtractErrorHeaders(error) {
|
|
442
|
+
if (isRecord(error)) {
|
|
443
|
+
const headers = error['Headers'] ?? error['headers'];
|
|
444
|
+
if (isRecord(headers)) {
|
|
445
|
+
const out = {};
|
|
446
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
447
|
+
if (typeof v === 'string')
|
|
448
|
+
out[k.toLowerCase()] = v;
|
|
449
|
+
}
|
|
450
|
+
return out;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
/** Whether an error indicates a 429 rate-limit. */
|
|
456
|
+
IsRateLimitError(error) {
|
|
457
|
+
if (isRecord(error)) {
|
|
458
|
+
const status = error['Status'] ?? error['status'] ?? error['StatusCode'];
|
|
459
|
+
if (status === 429)
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
const msg = error instanceof Error ? error.message : String(error ?? '');
|
|
463
|
+
return /\b429\b/.test(msg);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Resolves the API key + tenant config from the linked Credential entity, falling back to the
|
|
467
|
+
* CompanyIntegration.Configuration / APIKey. The credential bytes never leave this scope.
|
|
468
|
+
*/
|
|
469
|
+
async LoadCredentials(companyIntegration, contextUser) {
|
|
470
|
+
let apiKey;
|
|
471
|
+
let accountId;
|
|
472
|
+
let tokenUrl;
|
|
473
|
+
let baseHost;
|
|
474
|
+
let apiVersion;
|
|
475
|
+
if (companyIntegration.CredentialID) {
|
|
476
|
+
const fromCred = await this.LoadFromCredentialEntity(companyIntegration.CredentialID, contextUser);
|
|
477
|
+
if (fromCred) {
|
|
478
|
+
apiKey = fromCred.ApiKey || apiKey;
|
|
479
|
+
accountId = fromCred.AccountId || accountId;
|
|
480
|
+
tokenUrl = fromCred.TokenUrl || tokenUrl;
|
|
481
|
+
baseHost = fromCred.BaseHost || baseHost;
|
|
482
|
+
apiVersion = fromCred.ApiVersion || apiVersion;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// Non-secret tenant config lives on Configuration JSON (AccountId, tokenUrl, host, version).
|
|
486
|
+
const configJson = companyIntegration.Configuration;
|
|
487
|
+
if (configJson) {
|
|
488
|
+
const fromConfig = this.ParseConfigJson(configJson);
|
|
489
|
+
if (fromConfig) {
|
|
490
|
+
apiKey = apiKey ?? fromConfig.ApiKey;
|
|
491
|
+
accountId = accountId ?? fromConfig.AccountId;
|
|
492
|
+
tokenUrl = tokenUrl ?? fromConfig.TokenUrl;
|
|
493
|
+
baseHost = baseHost ?? fromConfig.BaseHost;
|
|
494
|
+
apiVersion = apiVersion ?? fromConfig.ApiVersion;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// Legacy fallback: the API key may live on CompanyIntegration.APIKey.
|
|
498
|
+
apiKey = apiKey ?? companyIntegration.APIKey ?? undefined;
|
|
499
|
+
if (!apiKey) {
|
|
500
|
+
throw new Error('No Wild Apricot API key found — set the admin API Key on the credential, Configuration JSON, or CompanyIntegration.APIKey.');
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
ApiKey: apiKey,
|
|
504
|
+
AccountId: accountId,
|
|
505
|
+
TokenUrl: tokenUrl ?? WILDAPRICOT_TOKEN_URL,
|
|
506
|
+
BaseHost: baseHost,
|
|
507
|
+
ApiVersion: apiVersion,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
/** Loads credential fields from a Credential entity's Values JSON. */
|
|
511
|
+
async LoadFromCredentialEntity(credentialID, contextUser, provider) {
|
|
512
|
+
const md = provider ?? new Metadata();
|
|
513
|
+
const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
|
|
514
|
+
const loaded = await credential.Load(credentialID);
|
|
515
|
+
if (!loaded || !credential.Values)
|
|
516
|
+
return null;
|
|
517
|
+
return this.ParseConfigJson(credential.Values);
|
|
518
|
+
}
|
|
519
|
+
/** Parses a JSON string into credential/config fields (tolerant of casing/aliases). */
|
|
520
|
+
ParseConfigJson(json) {
|
|
521
|
+
try {
|
|
522
|
+
const result = WildApricotConfigSchema.safeParse(JSON.parse(json));
|
|
523
|
+
if (!result.success)
|
|
524
|
+
return null;
|
|
525
|
+
const p = result.data;
|
|
526
|
+
const apiKey = p.ApiKey ?? p.apiKey ?? p.APIKey ?? p.Key ?? p.Token ?? p.token;
|
|
527
|
+
const accountId = p.AccountId ?? p.accountId ?? p.AccountID;
|
|
528
|
+
const tokenUrl = p.tokenUrl ?? p.TokenUrl ?? p.tokenURL;
|
|
529
|
+
const baseHost = p.apiBaseUrl ?? p.BaseURL ?? p.baseHost;
|
|
530
|
+
const apiVersion = p.ApiVersion ?? p.apiVersion;
|
|
531
|
+
return {
|
|
532
|
+
ApiKey: apiKey ?? '',
|
|
533
|
+
AccountId: accountId != null ? String(accountId) : undefined,
|
|
534
|
+
TokenUrl: tokenUrl ?? WILDAPRICOT_TOKEN_URL,
|
|
535
|
+
BaseHost: baseHost,
|
|
536
|
+
ApiVersion: apiVersion,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
WildApricotConnector = __decorate([
|
|
545
|
+
RegisterClass(BaseIntegrationConnector, '@memberjunction/connector-wild-apricot')
|
|
546
|
+
], WildApricotConnector);
|
|
547
|
+
export { WildApricotConnector };
|
|
548
|
+
// ─── Module-level constants + helpers (mechanism, NOT a catalog) ──────────────
|
|
549
|
+
/** Wild Apricot API host root; the version segment (/v2.3) + tenant paths are appended at runtime. */
|
|
550
|
+
const WILDAPRICOT_API_HOST = 'https://api.wildapricot.org';
|
|
551
|
+
/** OAuth2 token endpoint (client_credentials). */
|
|
552
|
+
const WILDAPRICOT_TOKEN_URL = 'https://oauth.wildapricot.org/auth/token';
|
|
553
|
+
/** Default API version path segment. */
|
|
554
|
+
const DEFAULT_API_VERSION = 'v2.3';
|
|
555
|
+
/** Max page size — `$top` above 100 is silently clamped to 100 per spec. */
|
|
556
|
+
const WILDAPRICOT_MAX_PAGE_SIZE = 100;
|
|
557
|
+
/** The single object whose list endpoint is async (request → ResultId → poll). */
|
|
558
|
+
const CONTACT_OBJECT_NAME = 'Contact';
|
|
559
|
+
/** Async-contacts poll bounds: attempts × interval caps total wait so a stuck job cannot hang the sync. */
|
|
560
|
+
const POLL_MAX_ATTEMPTS = 60;
|
|
561
|
+
const POLL_INTERVAL_MS = 1000;
|
|
562
|
+
/** Zod schema for the credential/Configuration JSON shape (tolerant of casing aliases). */
|
|
563
|
+
const WildApricotConfigSchema = z.object({
|
|
564
|
+
ApiKey: z.string().optional(),
|
|
565
|
+
apiKey: z.string().optional(),
|
|
566
|
+
APIKey: z.string().optional(),
|
|
567
|
+
Key: z.string().optional(),
|
|
568
|
+
Token: z.string().optional(),
|
|
569
|
+
token: z.string().optional(),
|
|
570
|
+
AccountId: z.union([z.string(), z.number()]).optional(),
|
|
571
|
+
accountId: z.union([z.string(), z.number()]).optional(),
|
|
572
|
+
AccountID: z.union([z.string(), z.number()]).optional(),
|
|
573
|
+
tokenUrl: z.string().optional(),
|
|
574
|
+
TokenUrl: z.string().optional(),
|
|
575
|
+
tokenURL: z.string().optional(),
|
|
576
|
+
apiBaseUrl: z.string().optional(),
|
|
577
|
+
BaseURL: z.string().optional(),
|
|
578
|
+
baseHost: z.string().optional(),
|
|
579
|
+
ApiVersion: z.string().optional(),
|
|
580
|
+
apiVersion: z.string().optional(),
|
|
581
|
+
}).passthrough();
|
|
582
|
+
/** Zod schema for the async-contacts kick-off response (carries a ResultId to poll). */
|
|
583
|
+
const AsyncContactsSchema = z.object({
|
|
584
|
+
ResultId: z.string().optional(),
|
|
585
|
+
State: z.string().optional(),
|
|
586
|
+
}).passthrough();
|
|
587
|
+
/** Narrows an unknown value to a plain record. */
|
|
588
|
+
function isRecord(v) {
|
|
589
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
590
|
+
}
|
|
591
|
+
/** Small deterministic hash for record-identity fallback (FNV-1a, hex). */
|
|
592
|
+
function stableHash(record) {
|
|
593
|
+
const json = JSON.stringify(record, Object.keys(record).sort());
|
|
594
|
+
let h = 0x811c9dc5;
|
|
595
|
+
for (let i = 0; i < json.length; i++) {
|
|
596
|
+
h ^= json.charCodeAt(i);
|
|
597
|
+
h = Math.imul(h, 0x01000193);
|
|
598
|
+
}
|
|
599
|
+
return (h >>> 0).toString(16);
|
|
600
|
+
}
|
|
601
|
+
/** Awaits `ms` milliseconds (poll interval). */
|
|
602
|
+
function delay(ms) {
|
|
603
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Minimal bounded promise-pool: runs `worker` over `items` with at most `limit` in flight.
|
|
607
|
+
* (BaseRESTIntegrationConnector.RunBounded is private, so the sample-union override brings its own
|
|
608
|
+
* tiny pool — this is local plumbing, NOT a shared framework artifact.)
|
|
609
|
+
*/
|
|
610
|
+
async function runBounded(items, limit, worker) {
|
|
611
|
+
const queue = [...items];
|
|
612
|
+
const size = Math.max(1, Math.min(limit, queue.length));
|
|
613
|
+
const runners = Array.from({ length: size }, async () => {
|
|
614
|
+
for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
|
|
615
|
+
await worker(next);
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
await Promise.all(runners);
|
|
619
|
+
}
|
|
620
|
+
//# sourceMappingURL=WildApricotConnector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WildApricotConnector.js","sourceRoot":"","sources":["../src/WildApricotConnector.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from './WildApricotConnector.js';
|
|
2
|
+
/** Open App bootstrap entry: importing this module ran the connector's @RegisterClass decorator;
|
|
3
|
+
* this no-op satisfies the loader's required startupExport and forces the import at MJAPI boot. */
|
|
4
|
+
export declare function registerConnector(): void;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * from './WildApricotConnector.js';
|
|
2
|
+
/** Open App bootstrap entry: importing this module ran the connector's @RegisterClass decorator;
|
|
3
|
+
* this no-op satisfies the loader's required startupExport and forces the import at MJAPI boot. */
|
|
4
|
+
export function registerConnector() { }
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC;AAE1C;oGACoG;AACpG,MAAM,UAAU,iBAAiB,KAAiD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@memberjunction/connector-wild-apricot",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "MemberJunction WildApricot connector.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"/dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc && tsc-alias -f",
|
|
14
|
+
"test": "vitest run --passWithNoTests"
|
|
15
|
+
},
|
|
16
|
+
"author": "MemberJunction.com",
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@memberjunction/core": ">=5.42.0 <6.0.0",
|
|
20
|
+
"@memberjunction/core-entities": ">=5.42.0 <6.0.0",
|
|
21
|
+
"@memberjunction/global": ">=5.42.0 <6.0.0",
|
|
22
|
+
"@memberjunction/integration-engine": ">=5.42.0 <6.0.0"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@memberjunction/connector-schema-merge": "^1.0.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "24.10.11",
|
|
29
|
+
"tsc-alias": "^1.8.16",
|
|
30
|
+
"typescript": "^5.9.3",
|
|
31
|
+
"vitest": "^4.0.18",
|
|
32
|
+
"@memberjunction/core": "^5.42.0",
|
|
33
|
+
"@memberjunction/core-entities": "^5.42.0",
|
|
34
|
+
"@memberjunction/global": "^5.42.0",
|
|
35
|
+
"@memberjunction/integration-engine": "^5.42.0",
|
|
36
|
+
"@memberjunction/connector-schema-merge": "^1.0.0"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/MemberJunction/Integrations"
|
|
41
|
+
}
|
|
42
|
+
}
|