@memberjunction/connector-path-lms 1.0.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/PathLMSConnector.d.ts +394 -0
- package/dist/PathLMSConnector.js +1281 -0
- package/dist/PathLMSConnector.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,394 @@
|
|
|
1
|
+
import { type UserInfo } from '@memberjunction/core';
|
|
2
|
+
import type { MJCompanyIntegrationEntity } from '@memberjunction/core-entities';
|
|
3
|
+
import { BaseRESTIntegrationConnector, type RESTAuthContext, type RESTResponse, type PaginationState, type PaginationType, type ConnectionTestResult, type ExternalObjectSchema, type ExternalFieldSchema, type FetchContext, type FetchBatchResult } from '@memberjunction/integration-engine';
|
|
4
|
+
/**
|
|
5
|
+
* Path LMS Reporting API connector (Blue Sky eLearn).
|
|
6
|
+
*
|
|
7
|
+
* TRANSPORT: GraphQL over HTTP. There is exactly ONE protocol base — {@link BaseRESTIntegrationConnector};
|
|
8
|
+
* GraphQL rides on top of it. Every report query POSTs a GraphQL document to the single endpoint
|
|
9
|
+
* `https://data-api.pathlms.com/graphql`; {@link NormalizeResponse} strips the `data.<queryName>`
|
|
10
|
+
* envelope and surfaces `errors[]`.
|
|
11
|
+
*
|
|
12
|
+
* AUTH (two-step, no refresh token): {@link Authenticate} exchanges `{ applicationId, applicationSecret }`
|
|
13
|
+
* (form-urlencoded) at `https://data-api.pathlms.com/api/v1/getToken` for a ~12h bearer JWT. The token +
|
|
14
|
+
* expiry are cached per credential; on expiry OR a 401, the connector re-mints. applicationId/Secret come
|
|
15
|
+
* from the linked Credential entity (or the CompanyIntegration.Configuration JSON) — NEVER baked in code.
|
|
16
|
+
*
|
|
17
|
+
* PAGINATION: offset/limit (`PaginationType=Offset`) on the report queries.
|
|
18
|
+
*
|
|
19
|
+
* PULL-ONLY: the SDL documents 0 mutations / 0 subscriptions, so SupportsCreate/Update/Delete stay false
|
|
20
|
+
* (the base defaults) and no CRUD path is wired. No 501 stubs.
|
|
21
|
+
*
|
|
22
|
+
* INCREMENTAL: every IO here is `SupportsIncrementalSync=false` — the SDL's startDate/endDate args filter
|
|
23
|
+
* by event date, NOT record-modification time, so there is no watermark to assert (provable-only). The
|
|
24
|
+
* engine relies on content-hash idempotency + {@link StableOrderingKey} (the IO's stable `id`) for
|
|
25
|
+
* keyset-style resume.
|
|
26
|
+
*
|
|
27
|
+
* DISCOVERY — CREDENTIAL-FREE, FROM THE PUBLIC SCHEMA (the T3-deadlock fix):
|
|
28
|
+
* Path LMS publishes its complete GraphQL SDL credential-free as a SpectaQL HTML reference page at
|
|
29
|
+
* `https://data-api.pathlms.com/`. That page is the **schema-of-record**. {@link DiscoverObjects} and
|
|
30
|
+
* {@link DiscoverFields} FETCH + PARSE that public page WITH NO CREDENTIAL and enumerate the full standard
|
|
31
|
+
* universe of GraphQL **record types** (84 = 93 SDL object types − 9 non-record types). This is what makes
|
|
32
|
+
* the runtime credential-free `DocStructureSelfCheck` re-yield the same standard universe every time (so
|
|
33
|
+
* persisted objects never read as "structure drift").
|
|
34
|
+
*
|
|
35
|
+
* A live credential is strictly **ADDITIVE**: when present, {@link DiscoverFields} also runs a standard
|
|
36
|
+
* GraphQL introspection against `/graphql` and appends any tenant-specific fields the public SDL lacked
|
|
37
|
+
* (the `Discovered` extension). It NEVER samples live data at build time and NEVER becomes the baseline —
|
|
38
|
+
* the standard universe always comes from the public, token-free schema. The auth-gated live introspection
|
|
39
|
+
* being available does NOT make the connector "case 2": the same schema is published credential-free, so
|
|
40
|
+
* discovery is case 1 (public schema) + an additive case-2 tenant overlay. The catalog is NOT a module-level
|
|
41
|
+
* constant — it is fetched + parsed from the public page at discovery time (only the small set of non-record
|
|
42
|
+
* SDL types to *exclude* is a documented constant, NOT the catalog itself).
|
|
43
|
+
*/
|
|
44
|
+
export declare class PathLMSConnector extends BaseRESTIntegrationConnector {
|
|
45
|
+
/** Per-process token cache, keyed by credential identity (applicationId). Survives across fetches. */
|
|
46
|
+
private tokenCache;
|
|
47
|
+
/**
|
|
48
|
+
* Per-process cache of the parsed PUBLIC SpectaQL schema (type-name → record type + fields). Populated
|
|
49
|
+
* lazily, credential-free, from `https://data-api.pathlms.com/`. This is the standard-universe source of
|
|
50
|
+
* record for discovery; it is fetched + parsed at runtime, never a baked array.
|
|
51
|
+
*/
|
|
52
|
+
private publicSchemaCache;
|
|
53
|
+
private publicSchemaPromise;
|
|
54
|
+
/**
|
|
55
|
+
* Per-CompanyIntegration introspection cache of the live SDL's object types → their scalar/object field
|
|
56
|
+
* shape. Lets the GraphQL selection-set builder emit a valid sub-selection for object-valued (`json`)
|
|
57
|
+
* report fields like `attendees: [WebinarAttendee]!`, and feeds the ADDITIVE tenant-field overlay in
|
|
58
|
+
* DiscoverFields. Populated lazily at runtime from the live introspection query — never a baked catalog.
|
|
59
|
+
*/
|
|
60
|
+
private sdlTypeCache;
|
|
61
|
+
/** Verbatim ClassName / IntegrationName getter / MJ: Integrations.Name. */
|
|
62
|
+
get IntegrationName(): string;
|
|
63
|
+
/**
|
|
64
|
+
* KEYSET / no-watermark resume hint. None of the Path LMS report queries expose a record-modification
|
|
65
|
+
* watermark, so every object resumes by its stable ordering key — the object's declared primary key
|
|
66
|
+
* (the report's `id`). Resolved from the cached IOF PK. Returns null when no PK is declared (keyset
|
|
67
|
+
* resume unavailable for the PK-less roll-up/aggregate report types).
|
|
68
|
+
*/
|
|
69
|
+
StableOrderingKey(objectName: string): string | null;
|
|
70
|
+
/**
|
|
71
|
+
* Conservative rate-limit policy. Path LMS publishes no explicit per-app limit; a single GraphQL
|
|
72
|
+
* endpoint behind Apollo tolerates modest sustained throughput. A low default keeps the connector
|
|
73
|
+
* polite without a documented number to push to (provable-only — see PROVENANCE BatchRequestWaitTimeGap).
|
|
74
|
+
*/
|
|
75
|
+
get RateLimitPolicy(): {
|
|
76
|
+
TokensPerSec: number;
|
|
77
|
+
Burst?: number;
|
|
78
|
+
} | null;
|
|
79
|
+
/**
|
|
80
|
+
* Two-step token exchange. Resolves applicationId/applicationSecret from the credential, returns a
|
|
81
|
+
* cached non-expired token when available, else POSTs form-urlencoded credentials to /api/v1/getToken
|
|
82
|
+
* and caches the resulting bearer (stripping any leading "Bearer ") with a 12h expiry (minus a skew
|
|
83
|
+
* buffer). No refresh token exists — re-mint is just another exchange.
|
|
84
|
+
*/
|
|
85
|
+
protected Authenticate(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<PathLMSAuthContext>;
|
|
86
|
+
/**
|
|
87
|
+
* Builds an auth context from a directly-supplied/pre-configured bearer token WITHOUT a live token
|
|
88
|
+
* exchange. The transport-smoke gate (T7b) configures a dummy token and asserts the connector injects
|
|
89
|
+
* `Authorization: Bearer <token>` on the request — it must NOT require a successful /api/v1/getToken
|
|
90
|
+
* round-trip before a header is present. Whenever a credential/Configuration carries a direct bearer
|
|
91
|
+
* token (alias `Token`/`accessToken`/`bearerToken`/`apiKey`), {@link LoadCredentials} surfaces it as
|
|
92
|
+
* `PreconfiguredToken` and {@link GetOrMintToken} returns it verbatim, so {@link BuildHeaders} sets the
|
|
93
|
+
* header off the configured token state with no network call.
|
|
94
|
+
*/
|
|
95
|
+
/** Bearer header on every GraphQL request, plus JSON content negotiation. */
|
|
96
|
+
protected BuildHeaders(auth: RESTAuthContext): Record<string, string>;
|
|
97
|
+
/** The single GraphQL endpoint. APIPath on every IO is `/graphql`, so the base URL is the host root. */
|
|
98
|
+
protected GetBaseURL(companyIntegration: MJCompanyIntegrationEntity, _auth: RESTAuthContext): string;
|
|
99
|
+
/**
|
|
100
|
+
* Resolves the API host root: an optional `Configuration.BaseURL`/`GraphQLEndpoint` override (a
|
|
101
|
+
* self-hosted/sandbox Path LMS instance, or an e2e mock origin) else the canonical {@link PATHLMS_HOST}.
|
|
102
|
+
* A full `…/graphql` URL is tolerated and reduced to the host root (the connector appends the paths).
|
|
103
|
+
*/
|
|
104
|
+
private HostFor;
|
|
105
|
+
/**
|
|
106
|
+
* Executes an HTTP request via fetch. For GraphQL all requests are POSTs carrying a JSON
|
|
107
|
+
* `{ query, variables }` body. Parses the JSON body; returns the raw text on a non-JSON response.
|
|
108
|
+
*/
|
|
109
|
+
protected MakeHTTPRequest(_auth: RESTAuthContext, url: string, method: string, headers: Record<string, string>, body?: unknown): Promise<RESTResponse>;
|
|
110
|
+
/**
|
|
111
|
+
* Strips the GraphQL envelope: `{ data: { <responseDataKey>: <records> } }` → records, normalizing a
|
|
112
|
+
* single object to a one-element array. GraphQL errors are surfaced as a thrown error so the engine
|
|
113
|
+
* records the failure (rather than silently treating `data: null` as zero records). When `data` is
|
|
114
|
+
* present alongside `errors` (partial success), the data is returned and the error is left to the
|
|
115
|
+
* caller's HTTP-status handling.
|
|
116
|
+
*/
|
|
117
|
+
protected NormalizeResponse(rawBody: unknown, responseDataKey: string | null): Record<string, unknown>[];
|
|
118
|
+
/**
|
|
119
|
+
* Offset/limit pagination. Path LMS report queries return a flat list with no total-count or
|
|
120
|
+
* has-next signal, so "more pages remain" is inferred from a full page: when the page returned
|
|
121
|
+
* exactly `pageSize` records, there may be more (advance the offset); a short/empty page is the end.
|
|
122
|
+
*/
|
|
123
|
+
protected ExtractPaginationInfo(rawBody: unknown, paginationType: PaginationType, _currentPage: number, currentOffset: number, pageSize: number): PaginationState;
|
|
124
|
+
/**
|
|
125
|
+
* Verifies credentials by minting a token then issuing the cheapest GraphQL query the SDL documents
|
|
126
|
+
* (`teamsList { id }`). A 401/GraphQL auth error means bad credentials; a 2xx with a `data` envelope
|
|
127
|
+
* confirms both the token exchange and live GraphQL access.
|
|
128
|
+
*/
|
|
129
|
+
TestConnection(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<ConnectionTestResult>;
|
|
130
|
+
/**
|
|
131
|
+
* Enumerates the FULL STANDARD UNIVERSE of record types CREDENTIAL-FREE by fetching + parsing the
|
|
132
|
+
* public SpectaQL schema page at `https://data-api.pathlms.com/` — NOT a baked array and NOT dependent
|
|
133
|
+
* on any token or on the seeded metadata cache. This is the T3-deadlock fix: because the standard
|
|
134
|
+
* universe is sourced from the public schema (which always resolves without a credential), the runtime
|
|
135
|
+
* credential-free `DocStructureSelfCheck` re-yields the same universe and persisted objects never read
|
|
136
|
+
* as structure drift.
|
|
137
|
+
*
|
|
138
|
+
* The seeded Declared metadata cache is consulted only to ATTACH the persisted IntegrationObject `ID`
|
|
139
|
+
* to each discovered object (a join, not the source of the object set). A live credential is NOT used
|
|
140
|
+
* here at all — standard objects are token-free by construction.
|
|
141
|
+
*/
|
|
142
|
+
DiscoverObjects(companyIntegration: MJCompanyIntegrationEntity, _contextUser: UserInfo): Promise<ExternalObjectSchema[]>;
|
|
143
|
+
/**
|
|
144
|
+
* Returns the fields for an object as the FULL STANDARD set parsed CREDENTIAL-FREE from the public
|
|
145
|
+
* SpectaQL schema (the baseline), enriched by the seeded Declared metadata (PK/FK/type curation) where
|
|
146
|
+
* it exists, then — only when a live credential is present — ADDITIVELY augmented with any tenant-
|
|
147
|
+
* specific fields the live introspection exposes that the public SDL lacked (the `Discovered` overlay).
|
|
148
|
+
*
|
|
149
|
+
* The public-schema field set is the baseline that always resolves token-free. The Declared overlay
|
|
150
|
+
* never replaces it; the live overlay only appends. Introspection failures degrade gracefully to the
|
|
151
|
+
* public + Declared fields. NEVER samples live data; NEVER hardcodes the field catalog.
|
|
152
|
+
*/
|
|
153
|
+
DiscoverFields(companyIntegration: MJCompanyIntegrationEntity, objectName: string, contextUser: UserInfo): Promise<ExternalFieldSchema[]>;
|
|
154
|
+
/**
|
|
155
|
+
* Appends any live-introspection fields the public schema lacked (tenant `Discovered` overlay). The
|
|
156
|
+
* public + Declared fields are the floor; the live introspection only ADDS. Degrades to the merged
|
|
157
|
+
* baseline on any introspection failure.
|
|
158
|
+
*/
|
|
159
|
+
private AppendLiveFields;
|
|
160
|
+
/**
|
|
161
|
+
* Fetches records by POSTing a GraphQL document built from the IO's metadata:
|
|
162
|
+
* - operation name + return type + arguments from the IO's per-object Configuration JSON,
|
|
163
|
+
* - the selection set from the IOF field names (scalar fields selected directly; object-valued
|
|
164
|
+
* `json` fields given a one-level sub-selection resolved from the live SDL type map),
|
|
165
|
+
* - offset/limit appended for Offset-paginated queries.
|
|
166
|
+
*
|
|
167
|
+
* Full-record pass-through: every record's COMPLETE GraphQL node lands in `ExternalRecord.Fields` so
|
|
168
|
+
* the framework's custom-column capture sees every key. Identity is the IOF-declared PK; the base
|
|
169
|
+
* content-hash fallback handles PK-less / object-container objects.
|
|
170
|
+
*/
|
|
171
|
+
FetchChanges(ctx: FetchContext): Promise<FetchBatchResult>;
|
|
172
|
+
/**
|
|
173
|
+
* Fetches a NESTED record type by querying its entry "door" and descending the access-path field
|
|
174
|
+
* chain to the leaf record collection (tables ≠ doors). One GraphQL request selects down the path;
|
|
175
|
+
* the response is then walked the same path to flatten out every leaf record. Each leaf carries its
|
|
176
|
+
* FULL node in `Fields` (custom-column pass-through). Reporting doors return the whole set (no
|
|
177
|
+
* offset/limit on the nested path), so HasMore is false.
|
|
178
|
+
*/
|
|
179
|
+
private FetchViaAccessPath;
|
|
180
|
+
/** Wraps the leaf selection in the door + nested field chain: `door { seg1 { seg2 { leaf } } }`. */
|
|
181
|
+
private BuildNestedQuery;
|
|
182
|
+
/**
|
|
183
|
+
* Walks a GraphQL response down the access path (`data[door]` → each segment), flattening arrays and
|
|
184
|
+
* single objects at every hop, to yield the flat list of leaf records. Robust to a door/segment that
|
|
185
|
+
* resolves to either an object or an array.
|
|
186
|
+
*/
|
|
187
|
+
private DescendAccessPath;
|
|
188
|
+
/** Returns the cached public schema, fetching + parsing it once per process (credential-free). */
|
|
189
|
+
private GetPublicSchema;
|
|
190
|
+
/**
|
|
191
|
+
* Fetches the public SpectaQL HTML (no credential) and parses its `definition-object` blocks into
|
|
192
|
+
* record types + fields. This is the standard-universe source of record. Allows a subclass / test to
|
|
193
|
+
* override {@link FetchPublicSchemaHTML} to supply a fixture without a network call.
|
|
194
|
+
*/
|
|
195
|
+
private FetchAndParsePublicSchema;
|
|
196
|
+
/**
|
|
197
|
+
* Fetches the raw public SpectaQL schema HTML from `https://data-api.pathlms.com/` with NO auth header.
|
|
198
|
+
* Overridable seam (tests inject a fixture). Throws on a non-2xx response so discovery surfaces a real
|
|
199
|
+
* fetch failure rather than silently returning zero objects.
|
|
200
|
+
*/
|
|
201
|
+
protected FetchPublicSchemaHTML(): Promise<string>;
|
|
202
|
+
/**
|
|
203
|
+
* Builds the selection set string for the query's return type. Scalar IOF fields are selected
|
|
204
|
+
* directly; object-valued (`json`) IOF fields receive a one-level sub-selection of their SDL type's
|
|
205
|
+
* SCALAR fields when the live type map resolves the field's type. When the type can't be resolved
|
|
206
|
+
* (no introspection / open JSON scalar), the field is selected as a leaf — correct for true scalars,
|
|
207
|
+
* and harmless for unresolved fields (the query simply omits an unresolvable object field rather than
|
|
208
|
+
* sending an invalid leaf selection on a known object type).
|
|
209
|
+
*/
|
|
210
|
+
private BuildSelectionSet;
|
|
211
|
+
/** Builds a one-level scalar sub-selection for an object type (no nested object recursion). */
|
|
212
|
+
private BuildScalarSubSelection;
|
|
213
|
+
/**
|
|
214
|
+
* Assembles the full GraphQL query document and its variables. Operation arguments come from the IO's
|
|
215
|
+
* Configuration `OperationArguments` (e.g. `["limit:Int","offset:Int","teamIds:[Int!]"]`). Only the
|
|
216
|
+
* pagination args (offset/limit) are bound at fetch time when paginating; all other documented filter
|
|
217
|
+
* args are declared as optional variables left unset (the source returns the unfiltered report).
|
|
218
|
+
*/
|
|
219
|
+
private BuildQueryDocument;
|
|
220
|
+
/** Returns the cached/freshly-introspected SDL type map; throws on hard introspection failure. */
|
|
221
|
+
private GetSDLTypeMap;
|
|
222
|
+
/** Best-effort SDL type map — returns an empty map (no augmentation) on any introspection failure. */
|
|
223
|
+
private GetSDLTypeMapSafe;
|
|
224
|
+
/** Parses a GraphQL `__schema.types` introspection response into a name→SDLType map (OBJECT types only). */
|
|
225
|
+
private ParseIntrospection;
|
|
226
|
+
/** True when a field's underlying named type is an OBJECT type present in the map (needs sub-selection). */
|
|
227
|
+
private IsObjectField;
|
|
228
|
+
/** Returns the field's underlying OBJECT type name when it is an object (else null). */
|
|
229
|
+
private ObjectFieldTypeName;
|
|
230
|
+
/** Maps an SDL field's underlying type into the connector's coarse DataType vocabulary. */
|
|
231
|
+
private SDLTypeToDataType;
|
|
232
|
+
private IsListOrObjectScalar;
|
|
233
|
+
/** POSTs a `{ query, variables }` document to the GraphQL endpoint with auth headers. */
|
|
234
|
+
private PostGraphQL;
|
|
235
|
+
/** Throws on a non-2xx HTTP status or a GraphQL error with no usable data. */
|
|
236
|
+
private AssertGraphQLOK;
|
|
237
|
+
/**
|
|
238
|
+
* Resolves the bearer token for these credentials. Precedence:
|
|
239
|
+
* 1. A directly-supplied/pre-configured bearer token (`PreconfiguredToken`) is returned verbatim with
|
|
240
|
+
* NO network exchange — this is the path the transport-smoke gate (T7b) exercises with a dummy token,
|
|
241
|
+
* and the path a broker uses when it injects a ready bearer rather than appId/secret.
|
|
242
|
+
* 2. Else a cached, non-expired minted token.
|
|
243
|
+
* 3. Else mint a fresh token via the two-step /api/v1/getToken exchange and cache it (12h, minus skew).
|
|
244
|
+
*/
|
|
245
|
+
private GetOrMintToken;
|
|
246
|
+
/**
|
|
247
|
+
* Exchanges applicationId/applicationSecret for a bearer token via a form-urlencoded POST to
|
|
248
|
+
* /api/v1/getToken. The response carries `{ token: "Bearer <jwt>" }`; the leading "Bearer " is
|
|
249
|
+
* stripped so BuildHeaders applies a single prefix.
|
|
250
|
+
*/
|
|
251
|
+
private MintToken;
|
|
252
|
+
/** Reads the token from the getToken response and strips any leading "Bearer " prefix. */
|
|
253
|
+
private ExtractToken;
|
|
254
|
+
/**
|
|
255
|
+
* Resolves applicationId/applicationSecret from the linked Credential entity, falling back to the
|
|
256
|
+
* CompanyIntegration.Configuration JSON. Credentials are issued by Blue Sky eLearn and are NEVER
|
|
257
|
+
* hardcoded in connector code.
|
|
258
|
+
*/
|
|
259
|
+
private LoadCredentials;
|
|
260
|
+
/** Loads credentials from a Credential entity's Values JSON. */
|
|
261
|
+
private LoadFromCredentialEntity;
|
|
262
|
+
/**
|
|
263
|
+
* Parses a JSON string into credentials (tolerant of casing aliases). Accepts EITHER a pre-minted bearer
|
|
264
|
+
* token (`Token`/`accessToken`/`bearerToken`/`apiKey` — the exchange-free path the transport-smoke gate
|
|
265
|
+
* uses) OR an applicationId + applicationSecret pair for the two-step exchange. Returns null only when
|
|
266
|
+
* neither a usable token nor a full appId+secret pair is present.
|
|
267
|
+
*/
|
|
268
|
+
private ParseCredentialJson;
|
|
269
|
+
/** Gets an IO from the cache without throwing (used by StableOrderingKey, which may be called early). */
|
|
270
|
+
private TryGetCachedObject;
|
|
271
|
+
/** Returns the seeded Declared IO rows for this integration keyed by lower-cased name (safe on empty cache). */
|
|
272
|
+
private CachedObjectsByName;
|
|
273
|
+
/** Returns the seeded Declared fields for an object as ExternalFieldSchema (empty when not cached). */
|
|
274
|
+
private DeclaredFields;
|
|
275
|
+
/**
|
|
276
|
+
* Overlays the seeded Declared field curation (PK / FK target / type / required) onto the public-schema
|
|
277
|
+
* baseline, by field name. Declared wins for the curated structural attributes where it has an opinion;
|
|
278
|
+
* the public-schema baseline supplies everything else and guarantees the full field set even when no
|
|
279
|
+
* Declared row exists. Never drops a public field; only enriches.
|
|
280
|
+
*/
|
|
281
|
+
private MergeDeclaredOverlay;
|
|
282
|
+
/**
|
|
283
|
+
* Maps a parsed public-schema record field into the ExternalFieldSchema baseline shape — applying the
|
|
284
|
+
* EXACT id-PK / typed-reference-FK rules the persisted metadata was built from, so the connector's
|
|
285
|
+
* credential-free discovery reproduces the persisted PK/FK set and T3 `DocStructureSelfCheck` cannot
|
|
286
|
+
* drift. This is run with zero credential and uses ONLY the parsed public SDL universe ({@param schema}).
|
|
287
|
+
*
|
|
288
|
+
* PK: the row's own `id` field is the sole primary key. An id-less type has NO PK (the engine's
|
|
289
|
+
* content-hash carries identity). A `*Id` reference is NEVER a PK — it is a foreign key (below).
|
|
290
|
+
*
|
|
291
|
+
* FK (two cases, mirroring `parse-sdl-fk.mjs` + the id-less `*Id` demotion path):
|
|
292
|
+
* (1) TYPED REFERENCE — the field's unwrapped SDL type resolves to ANOTHER emitted record type
|
|
293
|
+
* (e.g. `groups: [Group]` → Group, `assessmentsReport: [Assessment]!` → Assessment).
|
|
294
|
+
* (2) SCALAR `<Type>Id` — a scalar field named `<emittedType>Id` (case-insensitive, e.g. userId→User,
|
|
295
|
+
* courseId→Course, webinarId→Webinar) where the capitalized stem matches another emitted record
|
|
296
|
+
* type. (No self-alias exclusion: the metadata marks even `userId`-same-as-id as an FK→User on the
|
|
297
|
+
* id-less report rows, so discovery must too.)
|
|
298
|
+
* Both cases exclude a self-reference to the field's own owning type.
|
|
299
|
+
*/
|
|
300
|
+
private PublicFieldToSchema;
|
|
301
|
+
/**
|
|
302
|
+
* Resolves a field's foreign-key target to another emitted record type, or null when the field is not
|
|
303
|
+
* a reference. Mirrors EXACTLY the rules `scripts/parse-sdl-fk.mjs` used to author the persisted metadata,
|
|
304
|
+
* so the connector's credential-free FK derivation reproduces it (T3 `DocStructureSelfCheck` cannot drift):
|
|
305
|
+
*
|
|
306
|
+
* (1) TYPED REFERENCE — the SDL anchor target is itself an emitted record type (e.g. `groups: [Group]`
|
|
307
|
+
* → Group). Always an FK; no contradiction exclusion.
|
|
308
|
+
* (2) SCALAR `<Type>Id` — the field name (sans trailing `Id`, capitalized) matches an emitted record
|
|
309
|
+
* type and the field's underlying type is a scalar (e.g. userId→User, courseId→Course, webinarId→
|
|
310
|
+
* Webinar). This IS an FK, with ONE exclusion: when the owning type has its own `id` primary key
|
|
311
|
+
* AND the field's prose marks it a self-alias of that id ("the userId field is the same as id…",
|
|
312
|
+
* "alias of id", "for cross referencing"), it is a renamed view of the row's own identity, not a
|
|
313
|
+
* reference to another row — the persisted metadata leaves those non-FK (e.g. `Order.userId`).
|
|
314
|
+
* On an id-LESS report row (`CategorySale.userId`, `InPersonEventUser.userId`, …) the `<Type>Id`
|
|
315
|
+
* IS the only identity/reference, so the self-alias exclusion does NOT apply and the FK is kept —
|
|
316
|
+
* exactly as the metadata records it.
|
|
317
|
+
*
|
|
318
|
+
* Returns the CANONICAL record-type name (so the FK target matches the persisted
|
|
319
|
+
* `@lookup:…Name=<Type>` exactly). Never resolves to the field's own owning type.
|
|
320
|
+
*/
|
|
321
|
+
private ResolveFKTarget;
|
|
322
|
+
/** Parses the per-IO Configuration JSON into the GraphQL query model (incl. the nested access path). */
|
|
323
|
+
private ParseIOConfiguration;
|
|
324
|
+
/** Returns the IOF PK field names (sorted by sequence), or empty when none is declared. */
|
|
325
|
+
private FindPKFieldNames;
|
|
326
|
+
/**
|
|
327
|
+
* Builds a stable record identity from the declared PK fields. When all PK parts are present, joins
|
|
328
|
+
* them with '|'; otherwise returns '' so the engine's content-hash identity fallback takes over (the
|
|
329
|
+
* `account`/`teams` container objects without a usable PK dedupe by content hash).
|
|
330
|
+
*/
|
|
331
|
+
private BuildRecordIdentity;
|
|
332
|
+
/** Resolves the page size for a fetch: explicit BatchSize, else the IO default, else 50. */
|
|
333
|
+
private ResolvePageSize;
|
|
334
|
+
/** NormalizeResponse variant that never throws — used by ExtractPaginationInfo to count a page. */
|
|
335
|
+
private NormalizeResponseSafe;
|
|
336
|
+
/** Converts an IntegrationObjectField entity to the ExternalFieldSchema shape (Declared discovery). */
|
|
337
|
+
private DeclaredFieldToSchema;
|
|
338
|
+
}
|
|
339
|
+
/** Auth context: resolved bearer token + the credentials used to mint it (for re-mint). */
|
|
340
|
+
interface PathLMSAuthContext extends RESTAuthContext {
|
|
341
|
+
Token: string;
|
|
342
|
+
Credentials: PathLMSCredentials;
|
|
343
|
+
/** Resolved host root for this connection (override or {@link PATHLMS_HOST}). */
|
|
344
|
+
BaseURL?: string;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Resolved Path LMS credentials. The normal path carries `applicationId` + `applicationSecret` (exchanged
|
|
348
|
+
* at /api/v1/getToken). `PreconfiguredToken` is an alternate, exchange-free path: a ready bearer token
|
|
349
|
+
* supplied directly (the transport-smoke dummy-token path, or a broker injecting a bearer). When
|
|
350
|
+
* `PreconfiguredToken` is set, appId/secret may be absent — {@link PathLMSConnector.GetOrMintToken} returns
|
|
351
|
+
* the token verbatim with no network call.
|
|
352
|
+
*/
|
|
353
|
+
interface PathLMSCredentials {
|
|
354
|
+
applicationId?: string;
|
|
355
|
+
applicationSecret?: string;
|
|
356
|
+
PreconfiguredToken?: string;
|
|
357
|
+
/** Optional host-root override (self-hosted/sandbox instance or e2e mock origin); absent ⇒ {@link PATHLMS_HOST}. */
|
|
358
|
+
BaseURL?: string;
|
|
359
|
+
}
|
|
360
|
+
/** A field parsed from the public SpectaQL schema. */
|
|
361
|
+
interface PublicField {
|
|
362
|
+
Name: string;
|
|
363
|
+
/** Underlying named type from the SDL link (list/non-null wrappers stripped), e.g. "Int", "Course". */
|
|
364
|
+
TargetType: string | null;
|
|
365
|
+
/** Whether the SDL type was a list (`[Foo]`). */
|
|
366
|
+
IsList: boolean;
|
|
367
|
+
/** Whether the SDL type was non-null at the top level (`!`). */
|
|
368
|
+
NonNull: boolean;
|
|
369
|
+
/** The field's documentation prose, if any. */
|
|
370
|
+
Description?: string;
|
|
371
|
+
}
|
|
372
|
+
/** A record type parsed from the public SpectaQL schema. */
|
|
373
|
+
interface PublicRecordType {
|
|
374
|
+
Name: string;
|
|
375
|
+
Description?: string;
|
|
376
|
+
Fields: PublicField[];
|
|
377
|
+
}
|
|
378
|
+
/** The fully-parsed public schema: the standard-universe record types + a by-name index. */
|
|
379
|
+
interface PublicSchema {
|
|
380
|
+
RecordTypes: PublicRecordType[];
|
|
381
|
+
RecordTypesByName: Map<string, PublicRecordType>;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Parses a SpectaQL HTML schema page into the standard universe of record types + fields. Each record type
|
|
385
|
+
* is a `<section id="definition-<Type>" class="definition definition-object">` block; the non-record SDL
|
|
386
|
+
* types in {@link NON_RECORD_SDL_TYPES} are removed. Each field row carries the property name + a typed
|
|
387
|
+
* link `<a href="#definition-<Target>"><code>[Type]!</code></a>` from which the underlying type, list-ness
|
|
388
|
+
* and non-null are read. This is the credential-free standard-universe enumeration (record TYPES, not the
|
|
389
|
+
* Query entry-point doors).
|
|
390
|
+
*/
|
|
391
|
+
export declare function parseSpectaQLSchema(html: string): PublicSchema;
|
|
392
|
+
/** Tree-shaking prevention function — import and call from the module entry point. */
|
|
393
|
+
export declare function LoadPathLMSConnector(): void;
|
|
394
|
+
export {};
|