@memberjunction/connector-higher-logic-vanilla 0.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.
|
@@ -0,0 +1,179 @@
|
|
|
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 CreateRecordContext, type CRUDResult, type RateLimitPolicy, type SourceSchemaInfo } from '@memberjunction/integration-engine';
|
|
4
|
+
/**
|
|
5
|
+
* Higher Logic Vanilla connector — extends BaseRESTIntegrationConnector (REST/JSON over HTTP).
|
|
6
|
+
*
|
|
7
|
+
* Discovery, the paginated GET loop, template-var read traversal (second-layer objects resolve their
|
|
8
|
+
* parent via metadata), and generic per-operation CRUD are inherited. This class supplies only the
|
|
9
|
+
* Vanilla-specific protocol surface: Bearer auth, the per-tenant base URL, numbered + Link-header
|
|
10
|
+
* pagination, the `dateUpdated`/`dateInserted` incremental filter, connection testing, PK-aware create
|
|
11
|
+
* ID extraction, named/composite path-var substitution, and the §7/§10 sync-efficiency hooks the frozen
|
|
12
|
+
* contract evidences.
|
|
13
|
+
*/
|
|
14
|
+
export declare class HigherLogicVanillaConnector extends BaseRESTIntegrationConnector {
|
|
15
|
+
/** Cached auth for the lifetime of a single sync run (Personal Access Tokens are long-lived, no refresh). */
|
|
16
|
+
private cachedAuth;
|
|
17
|
+
/**
|
|
18
|
+
* Response headers from the MOST RECENT MakeHTTPRequest, stashed so the (ctx-less) ExtractPaginationInfo
|
|
19
|
+
* can read the RFC-5988 `Link` header. Safe: the base pagination loop calls MakeHTTPRequest and
|
|
20
|
+
* ExtractPaginationInfo back-to-back with NO intervening await, so on a single-threaded event loop the
|
|
21
|
+
* stashed headers always correspond to the page just parsed.
|
|
22
|
+
*/
|
|
23
|
+
private lastResponseHeaders;
|
|
24
|
+
/**
|
|
25
|
+
* The active incremental filter fragment (e.g. `dateUpdated=%3E%3D<iso>`) for the object currently being
|
|
26
|
+
* fetched, stashed by the FetchChanges override so the (ctx-less) BuildPaginatedURL can append it. Set at
|
|
27
|
+
* the top of an incremental FetchChanges, cleared in its finally. Safe because the engine drives one
|
|
28
|
+
* FetchChanges per object at a time (single-threaded async; no concurrent BuildPaginatedURL for a
|
|
29
|
+
* different watermark).
|
|
30
|
+
*/
|
|
31
|
+
private activeWatermarkFilter;
|
|
32
|
+
/** Verbatim `MJ: Integrations.Name`. Load-bearing: the T1 three-way name check compares this === metadata Name. */
|
|
33
|
+
get IntegrationName(): string;
|
|
34
|
+
/**
|
|
35
|
+
* Sample-union enrichment (MJ connector standard): the Declared metadata is docs/spec-derived and misses
|
|
36
|
+
* a tenant's custom profile fields (Vanilla ProfileField extensions surface as extra keys on user/record
|
|
37
|
+
* reads) and addon-specific attributes. After the base cache-driven introspection, we sample each
|
|
38
|
+
* object's live read shape via `DiscoverFieldsViaFetch` and UNION it into the declared field set with
|
|
39
|
+
* `mergeDeclaredWithSampledFields` — never-shrink, declared-wins, capacities widened. Best-effort +
|
|
40
|
+
* parallel; a sample failure (no credential, addon disabled) leaves that object's declared fields intact.
|
|
41
|
+
* NOTE: we override `IntrospectSchema` (NOT `DiscoverFields` — that would recurse into
|
|
42
|
+
* `DiscoverFieldsViaFetch`'s own fallback). Connector-agnostic: no Vanilla-specific field logic.
|
|
43
|
+
*/
|
|
44
|
+
IntrospectSchema(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<SourceSchemaInfo>;
|
|
45
|
+
get SupportsCreate(): boolean;
|
|
46
|
+
get SupportsUpdate(): boolean;
|
|
47
|
+
get SupportsDelete(): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Discovery is NON-authoritative: DiscoverObjects / IntrospectSchema are cache-driven (they re-read
|
|
50
|
+
* persisted ACTIVE Declared metadata, NOT a live full-gamut enumeration). Vanilla exposes no
|
|
51
|
+
* describe/introspection endpoint enumerating everything a credential can access, and a tenant's custom
|
|
52
|
+
* ProfileFields are knowable only from data — they flow through the sample-union above + the framework's
|
|
53
|
+
* runtime custom-column capture. So absence in a refresh proves nothing → never deactivate. Matches
|
|
54
|
+
* Configuration.DiscoveryIsAuthoritative=false in the frozen contract.
|
|
55
|
+
*/
|
|
56
|
+
get DiscoveryIsAuthoritative(): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* From Configuration.RateLimitPolicy (KB article 44): GET is 300 req/min per IP (=5/s), writes 120/min
|
|
59
|
+
* (=2/s). A separate HARD limit — 250 requests / 10s — triggers a MANUAL-intervention IP block (not an
|
|
60
|
+
* auto-lifting 429), so we stay well under it: 5/s sustained (the GET ceiling) with a small burst. The
|
|
61
|
+
* engine's AIMD bucket backs off further on any 429.
|
|
62
|
+
*/
|
|
63
|
+
get RateLimitPolicy(): RateLimitPolicy | null;
|
|
64
|
+
/** Conservative in-flight cap. The per-minute + hard-block ceilings are the real limiters. */
|
|
65
|
+
get MaxConcurrencyHint(): number | null;
|
|
66
|
+
/**
|
|
67
|
+
* No-watermark objects resume by their StableOrderingKey — read from the IO metadata when the extractor
|
|
68
|
+
* emitted one, else the object's declared PK (Vanilla's `<object>ID`). Returns null when the object
|
|
69
|
+
* declares no stable key or the cache is unavailable (unit-test context).
|
|
70
|
+
*/
|
|
71
|
+
StableOrderingKey(objectName: string): string | null;
|
|
72
|
+
/**
|
|
73
|
+
* Resolves the Bearer token + the per-tenant community base URL from the linked Credential entity
|
|
74
|
+
* (preferred) or the CompanyIntegration Configuration JSON (fallback). Cached for the run.
|
|
75
|
+
*/
|
|
76
|
+
protected Authenticate(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<RESTAuthContext>;
|
|
77
|
+
/** Vanilla auth: the Personal Access Token as a Bearer header. A static string — no signing, no crypto. */
|
|
78
|
+
protected BuildHeaders(auth: RESTAuthContext): Record<string, string>;
|
|
79
|
+
/** HTTP transport (fetch). Owns the wire boundary; test subclasses override this to capture requests. */
|
|
80
|
+
protected MakeHTTPRequest(_auth: RESTAuthContext, url: string, method: string, headers: Record<string, string>, body?: unknown): Promise<RESTResponse>;
|
|
81
|
+
/**
|
|
82
|
+
* Vanilla list endpoints return a BARE JSON ARRAY of records at the response root (ResponseDataKey=null
|
|
83
|
+
* for almost every object). A few wrap under a key (e.g. ProductMessage → ResponseDataKey='data'). The
|
|
84
|
+
* get-one shape is a single bare object. Handles all three.
|
|
85
|
+
*/
|
|
86
|
+
protected NormalizeResponse(rawBody: unknown, responseDataKey: string | null): Record<string, unknown>[];
|
|
87
|
+
/**
|
|
88
|
+
* Vanilla pagination (Configuration.PaginationDefaults): numbered pages whose authoritative "more data"
|
|
89
|
+
* signal is the RFC-5988 `Link` response header (`<url>; rel="next"`), NOT a body field. We read the Link
|
|
90
|
+
* header from the stashed headers (with the `x-app-page-next-url` header as a secondary signal, and a
|
|
91
|
+
* body-length heuristic as the last-resort fallback). For PageNumber the next page is page+1 (Vanilla's
|
|
92
|
+
* next URL is deterministic); for the one Cursor-typed object we surface the next-page URL verbatim as the
|
|
93
|
+
* cursor so BuildPaginatedURL follows it directly.
|
|
94
|
+
*/
|
|
95
|
+
protected ExtractPaginationInfo(rawBody: unknown, paginationType: PaginationType, currentPage: number, _currentOffset: number, pageSize: number): PaginationState;
|
|
96
|
+
/**
|
|
97
|
+
* Per-tenant base host + version path: `{communityUrl}/api/v2`. Composed in Authenticate (honoring a
|
|
98
|
+
* Configuration BaseURL override for sandbox/mock redirection) and stashed on the auth context. The
|
|
99
|
+
* object APIPaths (`/discussions`, …) are appended by the base BuildFullURL.
|
|
100
|
+
*/
|
|
101
|
+
protected GetBaseURL(_companyIntegration: MJCompanyIntegrationEntity, auth: RESTAuthContext): string;
|
|
102
|
+
/**
|
|
103
|
+
* Vanilla pagination params: `page=<n>&limit=<size>` (NOT the base default `page`/`pageSize`). When the
|
|
104
|
+
* incremental watermark filter is active this run, it is appended (`dateUpdated=>=<iso>`). For the
|
|
105
|
+
* Cursor-typed object the base hands back a FULL next-page URL as the cursor — return it verbatim.
|
|
106
|
+
*/
|
|
107
|
+
protected BuildPaginatedURL(basePath: string, obj: MJIntegrationObjectEntity, page: number, _offset: number, cursor?: string, effectivePageSize?: number): string;
|
|
108
|
+
/**
|
|
109
|
+
* OVERRIDDEN to (1) inject the `<IncrementalWatermarkField>=>=<watermark>` list filter for objects that
|
|
110
|
+
* declare SupportsIncrementalSync + carry a watermark this run, and (2) EMIT the new watermark. The base
|
|
111
|
+
* flat/template fetch threads no watermark into the URL and returns no NewWatermarkValue, so the connector
|
|
112
|
+
* owns both. All fetching (numbered pagination, template-var parent traversal) is delegated to the base;
|
|
113
|
+
* this override only sets activeWatermarkFilter around the super call and computes the max on a drained batch.
|
|
114
|
+
*
|
|
115
|
+
* Partial-failure safety: NewWatermarkValue is emitted ONLY when the whole object is drained
|
|
116
|
+
* (HasMore=false). A mid-stream batch advances no watermark, so a failure between batches resumes from the
|
|
117
|
+
* unchanged prior watermark. The `>=` operator (not `>`) means the boundary record re-syncs each run —
|
|
118
|
+
* idempotent (content-hash dedup) and never loses a record that shares the boundary timestamp.
|
|
119
|
+
*/
|
|
120
|
+
FetchChanges(ctx: FetchContext): Promise<FetchBatchResult>;
|
|
121
|
+
/**
|
|
122
|
+
* OVERRIDDEN for two Vanilla create idiosyncrasies the generic path can't express:
|
|
123
|
+
* 1. Vanilla's created-record ID field is `<object>ID` (discussionID / commentID / userID), NOT a plain
|
|
124
|
+
* `id`, so the base's ExtractIDFromResponse (which scans for id/ID/externalID) would return undefined
|
|
125
|
+
* and BuildCreatedResult would FAIL every create. We read the created object's PK from the metadata PK
|
|
126
|
+
* field(s) instead.
|
|
127
|
+
* 2. Nested create paths carry PARENT template vars (`/groups/{id}/members`, `/badges/{id}/requests`) —
|
|
128
|
+
* filled here from the record's own Attributes.
|
|
129
|
+
* The flat request body (Vanilla wraps nothing) and the loud-on-empty-id BuildCreatedResult are preserved.
|
|
130
|
+
* Top-level creates (no parent vars) behave exactly as the generic path would.
|
|
131
|
+
*/
|
|
132
|
+
CreateRecord(ctx: CreateRecordContext): Promise<CRUDResult>;
|
|
133
|
+
/**
|
|
134
|
+
* OVERRIDDEN so the generic Update/Delete/Get path can template Vanilla's NAMED and NESTED path vars —
|
|
135
|
+
* `/discussions/{id}`, `/groups/{id}/members/{userID}`, `/badges/{id}/requests/{userID}` — which the base
|
|
136
|
+
* (which only replaces `{id}`/`{ExternalID}`) cannot. A single-var path takes the whole ExternalID; a
|
|
137
|
+
* multi-var (nested) path splits the composite `parent|child` ExternalID in path order.
|
|
138
|
+
*/
|
|
139
|
+
protected SubstituteIDInPath(path: string, externalID: string, idLocation: string | null): string;
|
|
140
|
+
/**
|
|
141
|
+
* Tests the connection by hitting the current-user endpoint (`/api/v2/users/me`). A 2xx confirms the token
|
|
142
|
+
* + community URL are valid; 401/403 → auth failure; anything else → error.
|
|
143
|
+
*/
|
|
144
|
+
TestConnection(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<ConnectionTestResult>;
|
|
145
|
+
/** Reads the Vanilla credential (token + community URL) from the linked Credential entity or Configuration. */
|
|
146
|
+
private loadCredentials;
|
|
147
|
+
/** Loads a credential row and parses its Values JSON. */
|
|
148
|
+
private loadFromCredentialEntity;
|
|
149
|
+
/** Extracts Vanilla credential fields from a credential/config JSON string (tolerant of absent/invalid). */
|
|
150
|
+
private parseCredentialJson;
|
|
151
|
+
/**
|
|
152
|
+
* Composes the per-tenant base URL `{communityUrl}/api/v2`. Honors an explicit absolute-URL override
|
|
153
|
+
* (endpoint/baseUrl already carrying `/api/v2`, or a sandbox/mock origin) verbatim; otherwise appends the
|
|
154
|
+
* constant version path. ZERO community URL is baked into this connector — it always comes from data.
|
|
155
|
+
*/
|
|
156
|
+
private resolveCommunityBaseURL;
|
|
157
|
+
/**
|
|
158
|
+
* Extracts the created record's ExternalID from the response using the object's METADATA PK field(s)
|
|
159
|
+
* (Vanilla's PK is `<object>ID`, not a plain `id`). Falls back to the base ExtractIDFromResponse for
|
|
160
|
+
* header/body-`id` locations. Returns undefined when no PK value is present → BuildCreatedResult fails loudly.
|
|
161
|
+
*/
|
|
162
|
+
private extractCreatedID;
|
|
163
|
+
/** Substitutes CreateAPIPath parent template vars from the record's own attributes (nested creates). */
|
|
164
|
+
private substitutePathVarsFromAttributes;
|
|
165
|
+
/** Parses the RFC-5988 `Link` header (or `x-app-page-next-url`) for the `rel="next"` URL. */
|
|
166
|
+
private parseNextLink;
|
|
167
|
+
/**
|
|
168
|
+
* Max watermark across the batch's records for the given field, vs the prior watermark. ISO-8601
|
|
169
|
+
* timestamps compared by Date.parse (tolerant of differing offsets); the prior value is retained when no
|
|
170
|
+
* record carries a later timestamp (never regresses).
|
|
171
|
+
*/
|
|
172
|
+
private maxWatermark;
|
|
173
|
+
/** Joins a base URL and a path (mirrors the base's private BuildFullURL). */
|
|
174
|
+
private joinURL;
|
|
175
|
+
/** Gets an IO from the cache without throwing (used by StableOrderingKey, which may be called early). */
|
|
176
|
+
private tryGetCachedObject;
|
|
177
|
+
/** Returns the first present, non-empty string value among the given keys. */
|
|
178
|
+
private firstString;
|
|
179
|
+
}
|
|
@@ -0,0 +1,560 @@
|
|
|
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 { IntegrationEngineBase } from '@memberjunction/integration-engine-base';
|
|
10
|
+
import { BaseIntegrationConnector, BaseRESTIntegrationConnector, } from '@memberjunction/integration-engine';
|
|
11
|
+
import { mergeDeclaredWithSampledFields } from '@memberjunction/connector-schema-merge';
|
|
12
|
+
// ─── Design note ────────────────────────────────────────────────────────
|
|
13
|
+
//
|
|
14
|
+
// The connector is PURE MECHANISM. The object/field catalog is NOT baked here — it lives in the
|
|
15
|
+
// Declared metadata (metadata/integrations/higherlogic-vanilla/.higherlogic-vanilla.integration.json),
|
|
16
|
+
// seeded from Vanilla's credential-free OpenAPI/KB corpus (case-1 discovery). Discovery is INHERITED
|
|
17
|
+
// from BaseRESTIntegrationConnector: DiscoverObjects / DiscoverFields / IntrospectSchema read the
|
|
18
|
+
// persisted Declared metadata from the IntegrationEngineBase cache. There is NO hardcoded object list,
|
|
19
|
+
// NO field catalog, NO baked PK/FK/required/readonly/type constants in this file.
|
|
20
|
+
//
|
|
21
|
+
// What this connector implements (the Higher Logic Vanilla API v2 protocol shape over REST/JSON):
|
|
22
|
+
// - Auth: a Personal Access Token as a Bearer token — `Authorization: Bearer <token>` (KB article 41).
|
|
23
|
+
// A static, long-lived, manually-issued token — no OAuth handshake, no signing, no crypto. The SSO
|
|
24
|
+
// flavours the same platform also offers (OAuth2 / SAML / OIDC / jsConnect / JWT-addon) are END-USER
|
|
25
|
+
// web auth, NOT this server-to-server connector's path, and are deliberately NOT implemented.
|
|
26
|
+
// - Tenancy: Vanilla is per-tenant hosted (no single global host). Every request targets
|
|
27
|
+
// `{communityUrl}/api/v2/…`; the per-tenant {communityUrl} is read from the connection Configuration
|
|
28
|
+
// (PerTenantConfig.communityUrlConfigKey='communityUrl') / credential — ZERO community URL baked here.
|
|
29
|
+
// - Pagination: numbered pages (`page` + `limit`) whose authoritative "more data" signal is the RFC-5988
|
|
30
|
+
// `Link` response header (`rel="next"`) — with the `x-app-page-next-url` header and a body-length
|
|
31
|
+
// heuristic as fallbacks. NOT a body-embedded cursor.
|
|
32
|
+
// - Incremental: a `dateUpdated` (or insert-only `dateInserted`) list-endpoint filter — the per-object
|
|
33
|
+
// IncrementalWatermarkField from metadata. Sent as `<field>=>=<watermark>`; the new max is persisted
|
|
34
|
+
// only on a fully-drained batch (partial-failure-safe).
|
|
35
|
+
// - Write: generic per-operation CRUD (metadata-driven, flat bodies) for the write-capable IOs. Two
|
|
36
|
+
// documented idiosyncrasies are overridden: (1) Vanilla's PK is `<object>ID` (discussionID / userID),
|
|
37
|
+
// not a plain `id`, so create-ID extraction reads the metadata PK; (2) nested write paths carry named
|
|
38
|
+
// parent vars (`/groups/{id}/members/{userID}`) the base's single-`{id}` templating can't fill.
|
|
39
|
+
// ─── Constants ────────────────────────────────────────────────────────
|
|
40
|
+
/** Vanilla's API version path segment — constant across every per-tenant community host. */
|
|
41
|
+
const VANILLA_API_VERSION_PATH = '/api/v2';
|
|
42
|
+
/** Fallback page size when the IO declares no DefaultPageSize. */
|
|
43
|
+
const VANILLA_DEFAULT_LIMIT = 30;
|
|
44
|
+
/**
|
|
45
|
+
* Conservative universal cap on the requested `limit`. Endpoint maxima vary (100 for
|
|
46
|
+
* discussions/comments/categories, 500 for users — Configuration.PaginationDefaults.maxLimitSamples), but
|
|
47
|
+
* 100 is safe on every documented endpoint; a larger batch simply fetches more pages. This is a pagination
|
|
48
|
+
* bound, NOT a catalog — no per-object limit is hardcoded (the real sizes live in metadata / are discovered).
|
|
49
|
+
*/
|
|
50
|
+
const VANILLA_MAX_LIMIT = 100;
|
|
51
|
+
// ─── HigherLogicVanillaConnector ─────────────────────────────────────────
|
|
52
|
+
/**
|
|
53
|
+
* Higher Logic Vanilla connector — extends BaseRESTIntegrationConnector (REST/JSON over HTTP).
|
|
54
|
+
*
|
|
55
|
+
* Discovery, the paginated GET loop, template-var read traversal (second-layer objects resolve their
|
|
56
|
+
* parent via metadata), and generic per-operation CRUD are inherited. This class supplies only the
|
|
57
|
+
* Vanilla-specific protocol surface: Bearer auth, the per-tenant base URL, numbered + Link-header
|
|
58
|
+
* pagination, the `dateUpdated`/`dateInserted` incremental filter, connection testing, PK-aware create
|
|
59
|
+
* ID extraction, named/composite path-var substitution, and the §7/§10 sync-efficiency hooks the frozen
|
|
60
|
+
* contract evidences.
|
|
61
|
+
*/
|
|
62
|
+
let HigherLogicVanillaConnector = class HigherLogicVanillaConnector extends BaseRESTIntegrationConnector {
|
|
63
|
+
constructor() {
|
|
64
|
+
super(...arguments);
|
|
65
|
+
/** Cached auth for the lifetime of a single sync run (Personal Access Tokens are long-lived, no refresh). */
|
|
66
|
+
this.cachedAuth = null;
|
|
67
|
+
/**
|
|
68
|
+
* Response headers from the MOST RECENT MakeHTTPRequest, stashed so the (ctx-less) ExtractPaginationInfo
|
|
69
|
+
* can read the RFC-5988 `Link` header. Safe: the base pagination loop calls MakeHTTPRequest and
|
|
70
|
+
* ExtractPaginationInfo back-to-back with NO intervening await, so on a single-threaded event loop the
|
|
71
|
+
* stashed headers always correspond to the page just parsed.
|
|
72
|
+
*/
|
|
73
|
+
this.lastResponseHeaders = {};
|
|
74
|
+
/**
|
|
75
|
+
* The active incremental filter fragment (e.g. `dateUpdated=%3E%3D<iso>`) for the object currently being
|
|
76
|
+
* fetched, stashed by the FetchChanges override so the (ctx-less) BuildPaginatedURL can append it. Set at
|
|
77
|
+
* the top of an incremental FetchChanges, cleared in its finally. Safe because the engine drives one
|
|
78
|
+
* FetchChanges per object at a time (single-threaded async; no concurrent BuildPaginatedURL for a
|
|
79
|
+
* different watermark).
|
|
80
|
+
*/
|
|
81
|
+
this.activeWatermarkFilter = null;
|
|
82
|
+
}
|
|
83
|
+
// ── Identity (T1 three-way invariant) ────────────────────────────
|
|
84
|
+
/** Verbatim `MJ: Integrations.Name`. Load-bearing: the T1 three-way name check compares this === metadata Name. */
|
|
85
|
+
get IntegrationName() {
|
|
86
|
+
return 'higherlogic-vanilla';
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Sample-union enrichment (MJ connector standard): the Declared metadata is docs/spec-derived and misses
|
|
90
|
+
* a tenant's custom profile fields (Vanilla ProfileField extensions surface as extra keys on user/record
|
|
91
|
+
* reads) and addon-specific attributes. After the base cache-driven introspection, we sample each
|
|
92
|
+
* object's live read shape via `DiscoverFieldsViaFetch` and UNION it into the declared field set with
|
|
93
|
+
* `mergeDeclaredWithSampledFields` — never-shrink, declared-wins, capacities widened. Best-effort +
|
|
94
|
+
* parallel; a sample failure (no credential, addon disabled) leaves that object's declared fields intact.
|
|
95
|
+
* NOTE: we override `IntrospectSchema` (NOT `DiscoverFields` — that would recurse into
|
|
96
|
+
* `DiscoverFieldsViaFetch`'s own fallback). Connector-agnostic: no Vanilla-specific field logic.
|
|
97
|
+
*/
|
|
98
|
+
async IntrospectSchema(companyIntegration, contextUser) {
|
|
99
|
+
const info = await super.IntrospectSchema(companyIntegration, contextUser);
|
|
100
|
+
await Promise.all(info.Objects.map(async (obj) => {
|
|
101
|
+
try {
|
|
102
|
+
const sampled = await this.DiscoverFieldsViaFetch(companyIntegration, obj.ExternalName, contextUser);
|
|
103
|
+
obj.Fields = mergeDeclaredWithSampledFields(obj.Fields, sampled);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
/* best-effort — a sample failure leaves the declared fields as-is */
|
|
107
|
+
}
|
|
108
|
+
}));
|
|
109
|
+
return info;
|
|
110
|
+
}
|
|
111
|
+
// ── Capability getters (kept in lockstep with the per-op metadata columns) ──
|
|
112
|
+
// Write is a MIXED subset (Configuration.WriteCapability): many objects support create/update/delete,
|
|
113
|
+
// several are read-only, and a few support only a subset (e.g. BadgeRequest = create + delete, no
|
|
114
|
+
// update). The connector-level getters report the UNION (the connector CAN perform each verb); per-object
|
|
115
|
+
// null-column honesty is enforced by the base generic CRUD path, which THROWS for a verb whose
|
|
116
|
+
// Create/Update/Delete path+method columns are null (never silently sends a broken URL).
|
|
117
|
+
get SupportsCreate() { return true; }
|
|
118
|
+
get SupportsUpdate() { return true; }
|
|
119
|
+
get SupportsDelete() { return true; }
|
|
120
|
+
/**
|
|
121
|
+
* Discovery is NON-authoritative: DiscoverObjects / IntrospectSchema are cache-driven (they re-read
|
|
122
|
+
* persisted ACTIVE Declared metadata, NOT a live full-gamut enumeration). Vanilla exposes no
|
|
123
|
+
* describe/introspection endpoint enumerating everything a credential can access, and a tenant's custom
|
|
124
|
+
* ProfileFields are knowable only from data — they flow through the sample-union above + the framework's
|
|
125
|
+
* runtime custom-column capture. So absence in a refresh proves nothing → never deactivate. Matches
|
|
126
|
+
* Configuration.DiscoveryIsAuthoritative=false in the frozen contract.
|
|
127
|
+
*/
|
|
128
|
+
get DiscoveryIsAuthoritative() {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
// ── Sync-efficiency hooks (§7/§10 — populated from frozen-contract Configuration facts) ──
|
|
132
|
+
/**
|
|
133
|
+
* From Configuration.RateLimitPolicy (KB article 44): GET is 300 req/min per IP (=5/s), writes 120/min
|
|
134
|
+
* (=2/s). A separate HARD limit — 250 requests / 10s — triggers a MANUAL-intervention IP block (not an
|
|
135
|
+
* auto-lifting 429), so we stay well under it: 5/s sustained (the GET ceiling) with a small burst. The
|
|
136
|
+
* engine's AIMD bucket backs off further on any 429.
|
|
137
|
+
*/
|
|
138
|
+
get RateLimitPolicy() {
|
|
139
|
+
return { TokensPerSec: 5, Burst: 10 };
|
|
140
|
+
}
|
|
141
|
+
// ExtractRetryAfterMs: inherited default (undefined). Configuration.RateLimitPolicy.retryAfterHeaderDocumented
|
|
142
|
+
// is FALSE — Vanilla documents no Retry-After / X-RateLimit-* header on its 429 (confirmed by a direct scan
|
|
143
|
+
// of the merged OpenAPI spec's response headers). Nothing to parse reliably; the AIMD bucket backs off on
|
|
144
|
+
// the 429 regardless. Left as a soft gap for live-probe confirmation rather than guessing a header name.
|
|
145
|
+
/** Conservative in-flight cap. The per-minute + hard-block ceilings are the real limiters. */
|
|
146
|
+
get MaxConcurrencyHint() { return 3; }
|
|
147
|
+
/**
|
|
148
|
+
* No-watermark objects resume by their StableOrderingKey — read from the IO metadata when the extractor
|
|
149
|
+
* emitted one, else the object's declared PK (Vanilla's `<object>ID`). Returns null when the object
|
|
150
|
+
* declares no stable key or the cache is unavailable (unit-test context).
|
|
151
|
+
*/
|
|
152
|
+
StableOrderingKey(objectName) {
|
|
153
|
+
const obj = this.tryGetCachedObject(objectName);
|
|
154
|
+
if (!obj)
|
|
155
|
+
return null;
|
|
156
|
+
const declared = obj.StableOrderingKey;
|
|
157
|
+
if (declared && declared.trim().length > 0)
|
|
158
|
+
return declared.trim();
|
|
159
|
+
const pk = this.GetCachedFields(obj.ID).find(f => f.IsPrimaryKey);
|
|
160
|
+
return pk?.Name ?? null;
|
|
161
|
+
}
|
|
162
|
+
// ── Abstract REST hooks ──────────────────────────────────────────
|
|
163
|
+
/**
|
|
164
|
+
* Resolves the Bearer token + the per-tenant community base URL from the linked Credential entity
|
|
165
|
+
* (preferred) or the CompanyIntegration Configuration JSON (fallback). Cached for the run.
|
|
166
|
+
*/
|
|
167
|
+
async Authenticate(companyIntegration, contextUser) {
|
|
168
|
+
if (this.cachedAuth)
|
|
169
|
+
return this.cachedAuth;
|
|
170
|
+
const creds = await this.loadCredentials(companyIntegration, contextUser);
|
|
171
|
+
if (!creds.Token) {
|
|
172
|
+
throw new Error('[higherlogic-vanilla] No Personal Access Token found. Attach a credential carrying the token ' +
|
|
173
|
+
'(token / apiKey / personalAccessToken), or set it in the connection Configuration JSON.');
|
|
174
|
+
}
|
|
175
|
+
const baseURL = this.resolveCommunityBaseURL(companyIntegration, creds);
|
|
176
|
+
this.cachedAuth = { Token: creds.Token, CommunityBaseURL: baseURL };
|
|
177
|
+
return this.cachedAuth;
|
|
178
|
+
}
|
|
179
|
+
/** Vanilla auth: the Personal Access Token as a Bearer header. A static string — no signing, no crypto. */
|
|
180
|
+
BuildHeaders(auth) {
|
|
181
|
+
return {
|
|
182
|
+
'Authorization': `Bearer ${auth.Token}`,
|
|
183
|
+
'Content-Type': 'application/json',
|
|
184
|
+
'Accept': 'application/json',
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** HTTP transport (fetch). Owns the wire boundary; test subclasses override this to capture requests. */
|
|
188
|
+
async MakeHTTPRequest(_auth, url, method, headers, body) {
|
|
189
|
+
const response = await fetch(url, {
|
|
190
|
+
method,
|
|
191
|
+
headers,
|
|
192
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
193
|
+
});
|
|
194
|
+
const respHeaders = {};
|
|
195
|
+
response.headers.forEach((v, k) => { respHeaders[k.toLowerCase()] = v; });
|
|
196
|
+
this.lastResponseHeaders = respHeaders;
|
|
197
|
+
const text = await response.text();
|
|
198
|
+
let parsed = null;
|
|
199
|
+
if (text.length > 0) {
|
|
200
|
+
try {
|
|
201
|
+
parsed = JSON.parse(text);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
parsed = text;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { Status: response.status, Body: parsed, Headers: respHeaders };
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Vanilla list endpoints return a BARE JSON ARRAY of records at the response root (ResponseDataKey=null
|
|
211
|
+
* for almost every object). A few wrap under a key (e.g. ProductMessage → ResponseDataKey='data'). The
|
|
212
|
+
* get-one shape is a single bare object. Handles all three.
|
|
213
|
+
*/
|
|
214
|
+
NormalizeResponse(rawBody, responseDataKey) {
|
|
215
|
+
if (rawBody == null)
|
|
216
|
+
return [];
|
|
217
|
+
if (Array.isArray(rawBody))
|
|
218
|
+
return rawBody;
|
|
219
|
+
if (typeof rawBody !== 'object')
|
|
220
|
+
return [];
|
|
221
|
+
const body = rawBody;
|
|
222
|
+
if (responseDataKey) {
|
|
223
|
+
const arr = body[responseDataKey];
|
|
224
|
+
if (Array.isArray(arr))
|
|
225
|
+
return arr;
|
|
226
|
+
}
|
|
227
|
+
// get-one / non-list shape: the body IS the record.
|
|
228
|
+
return [body];
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Vanilla pagination (Configuration.PaginationDefaults): numbered pages whose authoritative "more data"
|
|
232
|
+
* signal is the RFC-5988 `Link` response header (`<url>; rel="next"`), NOT a body field. We read the Link
|
|
233
|
+
* header from the stashed headers (with the `x-app-page-next-url` header as a secondary signal, and a
|
|
234
|
+
* body-length heuristic as the last-resort fallback). For PageNumber the next page is page+1 (Vanilla's
|
|
235
|
+
* next URL is deterministic); for the one Cursor-typed object we surface the next-page URL verbatim as the
|
|
236
|
+
* cursor so BuildPaginatedURL follows it directly.
|
|
237
|
+
*/
|
|
238
|
+
ExtractPaginationInfo(rawBody, paginationType, currentPage, _currentOffset, pageSize) {
|
|
239
|
+
const nextURL = this.parseNextLink(this.lastResponseHeaders);
|
|
240
|
+
if (paginationType === 'Cursor') {
|
|
241
|
+
return nextURL ? { HasMore: true, NextCursor: nextURL } : { HasMore: false };
|
|
242
|
+
}
|
|
243
|
+
// PageNumber (the overwhelmingly common case).
|
|
244
|
+
if (nextURL) {
|
|
245
|
+
return { HasMore: true, NextPage: currentPage + 1 };
|
|
246
|
+
}
|
|
247
|
+
// Fallback when no Link/next header is present: a full page suggests more data. The KB warns a page
|
|
248
|
+
// CAN be short mid-stream (permission stripping), so the header is preferred; this only fires when the
|
|
249
|
+
// header is absent, and it stops on the first short/empty page (safe: worst case re-reads one page).
|
|
250
|
+
if (Array.isArray(rawBody) && pageSize > 0 && rawBody.length >= pageSize) {
|
|
251
|
+
return { HasMore: true, NextPage: currentPage + 1 };
|
|
252
|
+
}
|
|
253
|
+
return { HasMore: false };
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Per-tenant base host + version path: `{communityUrl}/api/v2`. Composed in Authenticate (honoring a
|
|
257
|
+
* Configuration BaseURL override for sandbox/mock redirection) and stashed on the auth context. The
|
|
258
|
+
* object APIPaths (`/discussions`, …) are appended by the base BuildFullURL.
|
|
259
|
+
*/
|
|
260
|
+
GetBaseURL(_companyIntegration, auth) {
|
|
261
|
+
return auth.CommunityBaseURL;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Vanilla pagination params: `page=<n>&limit=<size>` (NOT the base default `page`/`pageSize`). When the
|
|
265
|
+
* incremental watermark filter is active this run, it is appended (`dateUpdated=>=<iso>`). For the
|
|
266
|
+
* Cursor-typed object the base hands back a FULL next-page URL as the cursor — return it verbatim.
|
|
267
|
+
*/
|
|
268
|
+
BuildPaginatedURL(basePath, obj, page, _offset, cursor, effectivePageSize) {
|
|
269
|
+
// Cursor style (Link-header full-URL follow): the cursor IS the next page's absolute URL.
|
|
270
|
+
if (cursor && /^https?:\/\//i.test(cursor))
|
|
271
|
+
return cursor;
|
|
272
|
+
const requested = effectivePageSize ?? obj.DefaultPageSize ?? VANILLA_DEFAULT_LIMIT;
|
|
273
|
+
const limit = Math.max(1, Math.min(requested, VANILLA_MAX_LIMIT));
|
|
274
|
+
const parts = [`page=${page}`, `limit=${limit}`];
|
|
275
|
+
if (this.activeWatermarkFilter)
|
|
276
|
+
parts.push(this.activeWatermarkFilter);
|
|
277
|
+
const separator = basePath.includes('?') ? '&' : '?';
|
|
278
|
+
return `${basePath}${separator}${parts.join('&')}`;
|
|
279
|
+
}
|
|
280
|
+
// ── FetchChanges override (incremental dateUpdated/dateInserted filter + new-watermark emission) ──
|
|
281
|
+
/**
|
|
282
|
+
* OVERRIDDEN to (1) inject the `<IncrementalWatermarkField>=>=<watermark>` list filter for objects that
|
|
283
|
+
* declare SupportsIncrementalSync + carry a watermark this run, and (2) EMIT the new watermark. The base
|
|
284
|
+
* flat/template fetch threads no watermark into the URL and returns no NewWatermarkValue, so the connector
|
|
285
|
+
* owns both. All fetching (numbered pagination, template-var parent traversal) is delegated to the base;
|
|
286
|
+
* this override only sets activeWatermarkFilter around the super call and computes the max on a drained batch.
|
|
287
|
+
*
|
|
288
|
+
* Partial-failure safety: NewWatermarkValue is emitted ONLY when the whole object is drained
|
|
289
|
+
* (HasMore=false). A mid-stream batch advances no watermark, so a failure between batches resumes from the
|
|
290
|
+
* unchanged prior watermark. The `>=` operator (not `>`) means the boundary record re-syncs each run —
|
|
291
|
+
* idempotent (content-hash dedup) and never loses a record that shares the boundary timestamp.
|
|
292
|
+
*/
|
|
293
|
+
async FetchChanges(ctx) {
|
|
294
|
+
const obj = this.GetCachedObject(ctx.CompanyIntegration.IntegrationID, ctx.ObjectName);
|
|
295
|
+
const wmField = obj.IncrementalWatermarkField;
|
|
296
|
+
const incremental = obj.SupportsIncrementalSync
|
|
297
|
+
&& !!wmField
|
|
298
|
+
&& ctx.WatermarkValue != null
|
|
299
|
+
&& ctx.WatermarkValue.length > 0;
|
|
300
|
+
if (!incremental) {
|
|
301
|
+
return super.FetchChanges(ctx);
|
|
302
|
+
}
|
|
303
|
+
this.activeWatermarkFilter = `${encodeURIComponent(wmField)}=${encodeURIComponent(`>=${ctx.WatermarkValue}`)}`;
|
|
304
|
+
try {
|
|
305
|
+
const result = await super.FetchChanges(ctx);
|
|
306
|
+
if (!result.HasMore) {
|
|
307
|
+
result.NewWatermarkValue = this.maxWatermark(result, wmField, ctx.WatermarkValue);
|
|
308
|
+
}
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
this.activeWatermarkFilter = null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// ── CRUD ──────────────────────────────────────────────────────────
|
|
316
|
+
//
|
|
317
|
+
// Top-level single-`{id}` objects (Discussion, Comment, User, Category, Article, …) use the INHERITED
|
|
318
|
+
// generic UpdateRecord / DeleteRecord / GetRecord unchanged — they read the per-operation IO columns and
|
|
319
|
+
// route the single `{id}` through the SubstituteIDInPath override below. CreateRecord is overridden for two
|
|
320
|
+
// Vanilla idiosyncrasies (PK-aware id extraction + nested-create parent vars); SubstituteIDInPath is
|
|
321
|
+
// overridden so the nested objects' named/composite path vars (`/groups/{id}/members/{userID}`) resolve.
|
|
322
|
+
/**
|
|
323
|
+
* OVERRIDDEN for two Vanilla create idiosyncrasies the generic path can't express:
|
|
324
|
+
* 1. Vanilla's created-record ID field is `<object>ID` (discussionID / commentID / userID), NOT a plain
|
|
325
|
+
* `id`, so the base's ExtractIDFromResponse (which scans for id/ID/externalID) would return undefined
|
|
326
|
+
* and BuildCreatedResult would FAIL every create. We read the created object's PK from the metadata PK
|
|
327
|
+
* field(s) instead.
|
|
328
|
+
* 2. Nested create paths carry PARENT template vars (`/groups/{id}/members`, `/badges/{id}/requests`) —
|
|
329
|
+
* filled here from the record's own Attributes.
|
|
330
|
+
* The flat request body (Vanilla wraps nothing) and the loud-on-empty-id BuildCreatedResult are preserved.
|
|
331
|
+
* Top-level creates (no parent vars) behave exactly as the generic path would.
|
|
332
|
+
*/
|
|
333
|
+
async CreateRecord(ctx) {
|
|
334
|
+
const ci = ctx.CompanyIntegration;
|
|
335
|
+
const contextUser = ctx.ContextUser;
|
|
336
|
+
const obj = this.GetCachedObject(ci.IntegrationID, ctx.ObjectName);
|
|
337
|
+
if (!obj.CreateAPIPath || !obj.CreateMethod) {
|
|
338
|
+
// Delegate so the "not configured" error is raised consistently by the base.
|
|
339
|
+
return super.CreateRecord(ctx);
|
|
340
|
+
}
|
|
341
|
+
const fields = this.GetCachedFields(obj.ID);
|
|
342
|
+
const auth = await this.Authenticate(ci, contextUser);
|
|
343
|
+
const baseURL = this.GetBaseURL(ci, auth);
|
|
344
|
+
const headers = this.BuildHeaders(auth);
|
|
345
|
+
const path = this.substitutePathVarsFromAttributes(obj.CreateAPIPath, ctx.Attributes);
|
|
346
|
+
const url = this.joinURL(baseURL, path);
|
|
347
|
+
const body = this.BuildOperationBody(ctx.Attributes, obj.CreateBodyShape, obj.CreateBodyKey);
|
|
348
|
+
const response = await this.MakeHTTPRequest(auth, url, obj.CreateMethod, headers, body);
|
|
349
|
+
if (response.Status >= 200 && response.Status < 300) {
|
|
350
|
+
const externalID = this.extractCreatedID(obj, fields, response);
|
|
351
|
+
return this.BuildCreatedResult(externalID, response.Status, ctx.ObjectName);
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
Success: false,
|
|
355
|
+
StatusCode: response.Status,
|
|
356
|
+
ErrorMessage: this.ExtractErrorMessage(response) ?? `HTTP ${response.Status} on create`,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* OVERRIDDEN so the generic Update/Delete/Get path can template Vanilla's NAMED and NESTED path vars —
|
|
361
|
+
* `/discussions/{id}`, `/groups/{id}/members/{userID}`, `/badges/{id}/requests/{userID}` — which the base
|
|
362
|
+
* (which only replaces `{id}`/`{ExternalID}`) cannot. A single-var path takes the whole ExternalID; a
|
|
363
|
+
* multi-var (nested) path splits the composite `parent|child` ExternalID in path order.
|
|
364
|
+
*/
|
|
365
|
+
SubstituteIDInPath(path, externalID, idLocation) {
|
|
366
|
+
if (idLocation && idLocation !== 'path')
|
|
367
|
+
return path;
|
|
368
|
+
const vars = path.match(/\{\w+\}/g);
|
|
369
|
+
if (!vars || vars.length === 0)
|
|
370
|
+
return path;
|
|
371
|
+
if (vars.length === 1)
|
|
372
|
+
return path.replace(vars[0], encodeURIComponent(externalID));
|
|
373
|
+
const parts = externalID.split('|');
|
|
374
|
+
let out = path;
|
|
375
|
+
vars.forEach((v, i) => {
|
|
376
|
+
const val = parts[i] ?? parts[parts.length - 1] ?? externalID;
|
|
377
|
+
out = out.replace(v, encodeURIComponent(val));
|
|
378
|
+
});
|
|
379
|
+
return out;
|
|
380
|
+
}
|
|
381
|
+
// ── Connection test ──────────────────────────────────────────────
|
|
382
|
+
/**
|
|
383
|
+
* Tests the connection by hitting the current-user endpoint (`/api/v2/users/me`). A 2xx confirms the token
|
|
384
|
+
* + community URL are valid; 401/403 → auth failure; anything else → error.
|
|
385
|
+
*/
|
|
386
|
+
async TestConnection(companyIntegration, contextUser) {
|
|
387
|
+
try {
|
|
388
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
389
|
+
const baseURL = this.GetBaseURL(companyIntegration, auth);
|
|
390
|
+
const headers = this.BuildHeaders(auth);
|
|
391
|
+
const url = this.joinURL(baseURL, '/users/me');
|
|
392
|
+
const response = await this.MakeHTTPRequest(auth, url, 'GET', headers);
|
|
393
|
+
if (response.Status >= 200 && response.Status < 300) {
|
|
394
|
+
return { Success: true, Message: 'Higher Logic Vanilla connection successful.' };
|
|
395
|
+
}
|
|
396
|
+
if (response.Status === 401 || response.Status === 403) {
|
|
397
|
+
return { Success: false, Message: `Higher Logic Vanilla authentication failed (HTTP ${response.Status}). Check the Personal Access Token and community URL.` };
|
|
398
|
+
}
|
|
399
|
+
return { Success: false, Message: `Higher Logic Vanilla connection test returned HTTP ${response.Status}.` };
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
403
|
+
return { Success: false, Message: `Higher Logic Vanilla connection test error: ${msg}` };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// ── Credential + tenant loading ──────────────────────────────────
|
|
407
|
+
/** Reads the Vanilla credential (token + community URL) from the linked Credential entity or Configuration. */
|
|
408
|
+
async loadCredentials(companyIntegration, contextUser) {
|
|
409
|
+
const credentialID = companyIntegration.CredentialID;
|
|
410
|
+
let creds = null;
|
|
411
|
+
if (credentialID) {
|
|
412
|
+
creds = await this.loadFromCredentialEntity(credentialID, contextUser);
|
|
413
|
+
}
|
|
414
|
+
const configCreds = companyIntegration.Configuration ? this.parseCredentialJson(companyIntegration.Configuration) : null;
|
|
415
|
+
// Credential store wins for the secret token; Configuration usually carries the (non-secret) community URL.
|
|
416
|
+
const merged = {
|
|
417
|
+
Token: creds?.Token ?? configCreds?.Token,
|
|
418
|
+
CommunityUrl: creds?.CommunityUrl ?? configCreds?.CommunityUrl,
|
|
419
|
+
};
|
|
420
|
+
if (!creds && !configCreds) {
|
|
421
|
+
throw new Error('[higherlogic-vanilla] No credential found. Attach a credential carrying the Personal Access Token ' +
|
|
422
|
+
'(+ the community URL), or set Configuration JSON.');
|
|
423
|
+
}
|
|
424
|
+
return merged;
|
|
425
|
+
}
|
|
426
|
+
/** Loads a credential row and parses its Values JSON. */
|
|
427
|
+
async loadFromCredentialEntity(credentialID, contextUser, provider) {
|
|
428
|
+
const md = provider ?? new Metadata();
|
|
429
|
+
const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
|
|
430
|
+
const loaded = await credential.Load(credentialID);
|
|
431
|
+
if (!loaded || !credential.Values)
|
|
432
|
+
return null;
|
|
433
|
+
return this.parseCredentialJson(credential.Values);
|
|
434
|
+
}
|
|
435
|
+
/** Extracts Vanilla credential fields from a credential/config JSON string (tolerant of absent/invalid). */
|
|
436
|
+
parseCredentialJson(json) {
|
|
437
|
+
try {
|
|
438
|
+
const parsed = JSON.parse(json);
|
|
439
|
+
return {
|
|
440
|
+
Token: this.firstString(parsed, ['token', 'Token', 'apiKey', 'ApiKey', 'personalAccessToken', 'PersonalAccessToken', 'accessToken', 'AccessToken']),
|
|
441
|
+
CommunityUrl: this.firstString(parsed, ['communityUrl', 'communityURL', 'CommunityUrl', 'CommunityURL', 'endpoint', 'Endpoint', 'baseUrl', 'baseURL', 'BaseUrl', 'BaseURL']),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Composes the per-tenant base URL `{communityUrl}/api/v2`. Honors an explicit absolute-URL override
|
|
450
|
+
* (endpoint/baseUrl already carrying `/api/v2`, or a sandbox/mock origin) verbatim; otherwise appends the
|
|
451
|
+
* constant version path. ZERO community URL is baked into this connector — it always comes from data.
|
|
452
|
+
*/
|
|
453
|
+
resolveCommunityBaseURL(_companyIntegration, creds) {
|
|
454
|
+
const raw = (creds.CommunityUrl ?? '').trim();
|
|
455
|
+
if (!raw) {
|
|
456
|
+
throw new Error('[higherlogic-vanilla] No community URL configured. Set the per-connection "communityUrl" ' +
|
|
457
|
+
'(the `https://<community-host>` tenant identifier) — it is never baked into the connector.');
|
|
458
|
+
}
|
|
459
|
+
const trimmed = raw.replace(/\/+$/, '');
|
|
460
|
+
// If the supplied value already carries the /api/v2 version path, use it as-is (override / already-composed).
|
|
461
|
+
if (/\/api\/v2$/i.test(trimmed))
|
|
462
|
+
return trimmed;
|
|
463
|
+
return `${trimmed}${VANILLA_API_VERSION_PATH}`;
|
|
464
|
+
}
|
|
465
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
466
|
+
/**
|
|
467
|
+
* Extracts the created record's ExternalID from the response using the object's METADATA PK field(s)
|
|
468
|
+
* (Vanilla's PK is `<object>ID`, not a plain `id`). Falls back to the base ExtractIDFromResponse for
|
|
469
|
+
* header/body-`id` locations. Returns undefined when no PK value is present → BuildCreatedResult fails loudly.
|
|
470
|
+
*/
|
|
471
|
+
extractCreatedID(obj, fields, response) {
|
|
472
|
+
const records = this.NormalizeResponse(response.Body, obj.ResponseDataKey);
|
|
473
|
+
const created = records[0];
|
|
474
|
+
if (created) {
|
|
475
|
+
const pkNames = fields.filter(f => f.IsPrimaryKey).sort((a, b) => a.Sequence - b.Sequence).map(f => f.Name);
|
|
476
|
+
if (pkNames.length > 0 && pkNames.every(n => created[n] != null && String(created[n]).length > 0)) {
|
|
477
|
+
return pkNames.map(n => String(created[n])).join('|');
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return this.ExtractIDFromResponse(response, obj.CreateIDLocation);
|
|
481
|
+
}
|
|
482
|
+
/** Substitutes CreateAPIPath parent template vars from the record's own attributes (nested creates). */
|
|
483
|
+
substitutePathVarsFromAttributes(path, attributes) {
|
|
484
|
+
return path.replace(/\{(\w+)\}/g, (match, name) => {
|
|
485
|
+
const v = attributes[name];
|
|
486
|
+
return v != null && String(v).length > 0 ? encodeURIComponent(String(v)) : match;
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
/** Parses the RFC-5988 `Link` header (or `x-app-page-next-url`) for the `rel="next"` URL. */
|
|
490
|
+
parseNextLink(headers) {
|
|
491
|
+
const direct = headers['x-app-page-next-url'];
|
|
492
|
+
if (typeof direct === 'string' && direct.trim().length > 0)
|
|
493
|
+
return direct.trim();
|
|
494
|
+
const link = headers['link'];
|
|
495
|
+
if (typeof link !== 'string' || link.length === 0)
|
|
496
|
+
return null;
|
|
497
|
+
// `<url1>; rel="first", <url2>; rel="next", <url3>; rel="last"`
|
|
498
|
+
for (const part of link.split(',')) {
|
|
499
|
+
const m = part.match(/<([^>]+)>\s*;\s*rel\s*=\s*"?next"?/i);
|
|
500
|
+
if (m)
|
|
501
|
+
return m[1].trim();
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Max watermark across the batch's records for the given field, vs the prior watermark. ISO-8601
|
|
507
|
+
* timestamps compared by Date.parse (tolerant of differing offsets); the prior value is retained when no
|
|
508
|
+
* record carries a later timestamp (never regresses).
|
|
509
|
+
*/
|
|
510
|
+
maxWatermark(result, wmField, prior) {
|
|
511
|
+
let bestStr = prior;
|
|
512
|
+
let bestMs = Date.parse(prior);
|
|
513
|
+
if (isNaN(bestMs))
|
|
514
|
+
bestMs = -Infinity;
|
|
515
|
+
for (const rec of result.Records) {
|
|
516
|
+
const raw = rec.Fields?.[wmField];
|
|
517
|
+
if (raw == null)
|
|
518
|
+
continue;
|
|
519
|
+
const s = String(raw);
|
|
520
|
+
const ms = Date.parse(s);
|
|
521
|
+
if (!isNaN(ms) && ms > bestMs) {
|
|
522
|
+
bestMs = ms;
|
|
523
|
+
bestStr = s;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return bestStr;
|
|
527
|
+
}
|
|
528
|
+
/** Joins a base URL and a path (mirrors the base's private BuildFullURL). */
|
|
529
|
+
joinURL(baseURL, apiPath) {
|
|
530
|
+
const base = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
|
|
531
|
+
const path = apiPath.startsWith('/') ? apiPath : `/${apiPath}`;
|
|
532
|
+
return `${base}${path}`;
|
|
533
|
+
}
|
|
534
|
+
/** Gets an IO from the cache without throwing (used by StableOrderingKey, which may be called early). */
|
|
535
|
+
tryGetCachedObject(objectName) {
|
|
536
|
+
try {
|
|
537
|
+
const integ = IntegrationEngineBase.Instance.GetIntegrationByName(this.IntegrationName);
|
|
538
|
+
if (!integ)
|
|
539
|
+
return null;
|
|
540
|
+
return IntegrationEngineBase.Instance.GetIntegrationObject(integ.ID, objectName) ?? null;
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/** Returns the first present, non-empty string value among the given keys. */
|
|
547
|
+
firstString(obj, keys) {
|
|
548
|
+
for (const k of keys) {
|
|
549
|
+
const v = obj[k];
|
|
550
|
+
if (typeof v === 'string' && v.length > 0)
|
|
551
|
+
return v;
|
|
552
|
+
}
|
|
553
|
+
return undefined;
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
HigherLogicVanillaConnector = __decorate([
|
|
557
|
+
RegisterClass(BaseIntegrationConnector, 'HigherLogicVanillaConnector')
|
|
558
|
+
], HigherLogicVanillaConnector);
|
|
559
|
+
export { HigherLogicVanillaConnector };
|
|
560
|
+
//# sourceMappingURL=HigherLogicVanillaConnector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HigherLogicVanillaConnector.js","sourceRoot":"","sources":["../src/HigherLogicVanillaConnector.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from './HigherLogicVanillaConnector.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 './HigherLogicVanillaConnector.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,kCAAkC,CAAC;AAEjD;oGACoG;AACpG,MAAM,UAAU,iBAAiB,KAAiD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@memberjunction/connector-higher-logic-vanilla",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MemberJunction HigherLogicVanilla connector.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"/dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc && tsc-alias -f",
|
|
13
|
+
"test": "vitest run"
|
|
14
|
+
},
|
|
15
|
+
"author": "MemberJunction.com",
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@memberjunction/core": ">=5.43.0 <6.0.0",
|
|
19
|
+
"@memberjunction/core-entities": ">=5.43.0 <6.0.0",
|
|
20
|
+
"@memberjunction/global": ">=5.43.0 <6.0.0",
|
|
21
|
+
"@memberjunction/integration-engine": ">=5.43.0 <6.0.0"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"zod": "~3.24.4",
|
|
25
|
+
"@memberjunction/connector-schema-merge": "^1.0.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@memberjunction/core": "^5.43.0",
|
|
29
|
+
"@memberjunction/core-entities": "^5.43.0",
|
|
30
|
+
"@memberjunction/global": "^5.43.0",
|
|
31
|
+
"@memberjunction/integration-engine": "^5.43.0",
|
|
32
|
+
"@types/node": "24.10.11",
|
|
33
|
+
"tsc-alias": "^1.8.16",
|
|
34
|
+
"typescript": "^5.9.3",
|
|
35
|
+
"vitest": "^4.0.18"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/MemberJunction/Integrations"
|
|
40
|
+
}
|
|
41
|
+
}
|