@memberjunction/connector-rasa-io 2.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.
@@ -0,0 +1,306 @@
1
+ import { type UserInfo } from '@memberjunction/core';
2
+ import type { MJCompanyIntegrationEntity, MJIntegrationObjectEntity } from '@memberjunction/core-entities';
3
+ import { BaseRESTIntegrationConnector, type ConnectionTestResult, type ExternalFieldSchema, type ExternalObjectSchema, type ExternalRecord, type FetchBatchResult, type FetchContext, type GetRecordContext, type PaginationState, type PaginationType, type RESTAuthContext, type RESTResponse, type SourceSchemaInfo } from '@memberjunction/integration-engine';
4
+ import { z } from 'zod';
5
+ declare const RasaConnectionConfigSchema: z.ZodObject<{
6
+ /** rasa.io API key presented in the token-exchange body. */
7
+ APIKey: z.ZodString;
8
+ /** Account username (email) for the token-exchange HTTP Basic header. */
9
+ Username: z.ZodString;
10
+ /** Account password for the token-exchange HTTP Basic header. */
11
+ Password: z.ZodString;
12
+ /** Optional host override (self-hosted / proxy / test double). Defaults to the vendor host root. */
13
+ BaseURL: z.ZodOptional<z.ZodString>;
14
+ }, "strip", z.ZodTypeAny, {
15
+ APIKey: string;
16
+ Username: string;
17
+ Password: string;
18
+ BaseURL?: string | undefined;
19
+ }, {
20
+ APIKey: string;
21
+ Username: string;
22
+ Password: string;
23
+ BaseURL?: string | undefined;
24
+ }>;
25
+ /** Connection configuration parsed from the MJ Credential (or CompanyIntegration.Configuration). */
26
+ export type RasaConnectionConfig = z.infer<typeof RasaConnectionConfigSchema>;
27
+ /**
28
+ * rasa.io connector (v1 + v2 REST, single host).
29
+ *
30
+ * Everything routine rides `BaseRESTIntegrationConnector`'s metadata-driven machinery — generic
31
+ * per-operation CRUD, pagination loop, template-var/parent iteration, record→ExternalRecord
32
+ * conversion. Four things are genuinely idiosyncratic and are the ONLY behavioural overrides:
33
+ *
34
+ * 1. **Two-step auth** — `POST /v1/tokens` (HTTP Basic + `{key}` body) mints a JWT that every
35
+ * subsequent request presents in a CUSTOM `rasa-token` header, not `Authorization: Bearer`.
36
+ * 2. **`skip`/`limit` + response-metadata paging** — the base emits `offset`/`limit`; rasa.io uses
37
+ * `skip`/`limit` (RealityProbe: "'skip' advanced past page 1 via offset") and drives the loop from
38
+ * `metadata.next_link`, whose own `skip` value is numeric for offset endpoints and an opaque token
39
+ * for others.
40
+ * 3. **`*_since` watermarks** — the incremental filter is a per-object query param
41
+ * (`updated_since` / `created_since` / `archived_since`) read from the frozen contract.
42
+ * 4. **Conditional per-record envelope unwrapping** — rasa.io wraps each record JSON:API-style as
43
+ * `{data, links}` on SOME objects. Driven strictly by the per-object `recordUnwrapPath` verdict;
44
+ * never guessed (see {@link NormalizeResponse}).
45
+ */
46
+ /** CANONICAL registration key — the repo's catalog convention is `ClassName` == the npm package name, so
47
+ * instance discovery matches. `Integration.ClassName` is seeded to this value.
48
+ * The short `RasaConnector` key below stays registered for continuity: `ConnectorFactory.Resolve` looks the
49
+ * Integration row's ClassName up verbatim in the ClassFactory, so any tenant row still carrying the legacy
50
+ * short name resolves rather than failing with "No connector registered". Zero cost to keep; removing it
51
+ * would be a breaking change independent of this release's rename. */
52
+ export declare class RasaConnector extends BaseRESTIntegrationConnector {
53
+ /** Cached JWT + mint time (idiosyncrasy #1). */
54
+ private cachedToken;
55
+ private tokenObtainedAt;
56
+ /** Response context for the in-flight fetch (see {@link RasaResponseContext}). */
57
+ private responseCtx;
58
+ /** Watermark value for the in-flight fetch — consumed by {@link AppendDefaultQueryParams}. */
59
+ private currentWatermark;
60
+ /** Non-fatal diagnostics raised during the in-flight fetch, drained into the FetchBatchResult. */
61
+ private pendingWarnings;
62
+ /** Running max of the watermark FIELD across the batches of one object's sync pass. */
63
+ private readonly watermarkHighWater;
64
+ /** Integration ID observed on the last operation — lets `StableOrderingKey(name)` reach the cache. */
65
+ private lastIntegrationID;
66
+ /** Merged public-spec cache (credential-free); populated lazily by discovery. */
67
+ private specCache;
68
+ /** Verbatim from the identity handoff / `MJ: Integrations.Name`. */
69
+ get IntegrationName(): string;
70
+ /** v1 `POST /persons|/posts|/lead-posts`, v2 `POST /lists|/contacts|/subscriptions` — all metadata-driven. */
71
+ get SupportsCreate(): boolean;
72
+ /** v1 `PUT /persons/{id}|/posts/{id}|/lead-posts`, v2 `PUT /contacts/{id}|/subscriptions/{id}`. */
73
+ get SupportsUpdate(): boolean;
74
+ /** v1 `DELETE /persons/{id}` (GDPR hard delete), v2 `DELETE /contacts/{id}` (archive). */
75
+ get SupportsDelete(): boolean;
76
+ /**
77
+ * `updated_since` / `created_since` are inclusive server-side filters over a monotonically
78
+ * advancing timestamp column, so the highest value seen is a safe resume point.
79
+ */
80
+ get MonotonicWatermark(): boolean;
81
+ /**
82
+ * Keyset hint the extractor emitted per object (`IntegrationObject.StableOrderingKey`). Returns the
83
+ * declared key, or null when the object has none — never a guess.
84
+ */
85
+ StableOrderingKey(objectName: string): string | null;
86
+ /**
87
+ * Step 1 of the vendor's documented flow: `POST {host}/v1/tokens` with an HTTP Basic
88
+ * `Authorization` header AND a `{ key: <apiKey> }` body; the JWT comes back at
89
+ * `results[0]['rasa-token']`. Step 2 (the custom header) is {@link BuildHeaders}.
90
+ */
91
+ protected Authenticate(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<RESTAuthContext>;
92
+ /** Mints (or reuses) the session JWT. Never logs any credential-derived value. */
93
+ private MintToken;
94
+ /**
95
+ * Reads the JWT out of the token envelope. Primary shape is the documented
96
+ * `ApiResonseTokenCreated` → `results[0]['rasa-token']` (`definitions.RasaToken`); the `data`-wrapped
97
+ * and v2 `access_token` shapes are accepted as transport tolerance, not as a schema claim.
98
+ */
99
+ private ReadTokenFromBody;
100
+ /** Step 2: the JWT rides a CUSTOM header (`securityDefinitions.authorizer`), not `Authorization`. */
101
+ protected BuildHeaders(auth: RESTAuthContext): Record<string, string>;
102
+ /** Host root for every request; APIPath supplies its own `/v1` or `/v2` segment. */
103
+ protected GetBaseURL(_companyIntegration: MJCompanyIntegrationEntity, auth: RESTAuthContext): string;
104
+ private HostOf;
105
+ /**
106
+ * HTTP transport with bounded retry: network blips, a 401 (mint a fresh JWT and replay once the
107
+ * cached one has aged out), and 429 honouring `Retry-After` when the vendor sends one.
108
+ */
109
+ protected MakeHTTPRequest(auth: RESTAuthContext, url: string, method: string, headers: Record<string, string>, body?: unknown): Promise<RESTResponse>;
110
+ private BuildRequestInit;
111
+ /** Normalizes a fetch Response, tolerating empty/non-JSON bodies (e.g. a 204 from DELETE). */
112
+ private ToRESTResponse;
113
+ /** `Retry-After` in seconds or as an HTTP-date; null when the vendor sent neither. */
114
+ private RetryAfterMs;
115
+ private IsRetriableNetworkError;
116
+ private Sleep;
117
+ /**
118
+ * Strips the rasa.io response envelope. TWO layers, both metadata-driven:
119
+ *
120
+ * 1. the LIST envelope — `{ code, event, metadata, results, status_code }` — via the object's
121
+ * `ResponseDataKey` (`results`, `results[].data`, `results[].lists`, …);
122
+ * 2. the PER-RECORD envelope — rasa.io wraps some records JSON:API-style as `{ data, links }` —
123
+ * via the object's `Configuration.recordUnwrapPath`, which materializes the RealityProbe's
124
+ * `recordEnvelopeShape` verdict. Present ⇒ verdict was `nested` ⇒ unwrap. **Absent ⇒ we do NOT
125
+ * unwrap** — a missing verdict is DEFERRED, never resolved as "flat" by assumption. When an
126
+ * un-verdicted object nonetheless arrives looking exactly like the vendor's envelope
127
+ * (`{data, links}` and nothing else), we emit a `RECORD_ENVELOPE_UNVERIFIED` warning naming the
128
+ * object so the gap is loud in the run artifact rather than silently mis-mapped.
129
+ */
130
+ protected NormalizeResponse(rawBody: unknown, responseDataKey: string | null): Record<string, unknown>[];
131
+ /**
132
+ * Evaluates a dotted extraction path against a response body. A segment may be a plain key or a
133
+ * `key[]` list segment; arrays are flattened either way, so `results`, `results[].data` and
134
+ * `results[].data.topics` all resolve uniformly. A null path returns the body itself (array or object).
135
+ */
136
+ private ExtractByPath;
137
+ /** Raises a one-per-fetch diagnostic when an un-verdicted object arrives inside the vendor envelope. */
138
+ private FlagUndeclaredEnvelope;
139
+ /**
140
+ * rasa.io pages with `skip` + `limit` (NOT the base's `offset`/`limit`). The `skip` value is a
141
+ * numeric offset on offset endpoints and an opaque token on the endpoints whose `next_link` carries
142
+ * a non-numeric `skip` — {@link ExtractPaginationInfo} classifies it, we replay whichever it gave us.
143
+ * `limit` is capped by the object's declared `DefaultPageSize` (the v1 spec's hard `maximum: 50`).
144
+ */
145
+ protected BuildPaginatedURL(basePath: string, obj: MJIntegrationObjectEntity, _page: number, offset: number, cursor?: string, effectivePageSize?: number): string;
146
+ /**
147
+ * Drives the loop from the response-metadata envelope
148
+ * (`PersonsApiResponseMetadata` / `InsightApiResponseMetadata` / v2 `ResponseMetadata`):
149
+ * `next_link` is the vendor's own next-page URL. A short page (fewer records than requested) always
150
+ * terminates — the vendor emits `next_link` past the end of the dataset, so it is not a sufficient
151
+ * stop signal on its own.
152
+ */
153
+ protected ExtractPaginationInfo(rawBody: unknown, _paginationType: PaginationType, _currentPage: number, currentOffset: number, pageSize: number): PaginationState;
154
+ /** Reads the `skip` query param out of a vendor `next_link`, tolerating a relative URL. */
155
+ private ReadSkipParam;
156
+ /**
157
+ * Appends the object's declared incremental filter — `Configuration.watermarkParam`
158
+ * (`updated_since` / `created_since` / `archived_since`) — to every request of a watermarked
159
+ * object. Applied HERE rather than in {@link BuildPaginatedURL} so it also reaches non-paginated
160
+ * single-page fetches. Never invents a param name: silent metadata ⇒ full pull.
161
+ */
162
+ protected AppendDefaultQueryParams(url: string, obj: MJIntegrationObjectEntity): string;
163
+ /**
164
+ * Thin wrapper around the base's metadata-driven fetch. It (a) publishes the per-object response
165
+ * context the base's object-less `NormalizeResponse` signature can't carry, (b) bridges the frozen
166
+ * contract's `accessPath.parentObject` onto the key the base's parent resolver reads, (c) tracks the
167
+ * watermark high-water mark across the object's batches and emits it ONLY on the terminal batch, so
168
+ * a mid-pass failure (which throws before we return) leaves the stored watermark untouched.
169
+ */
170
+ FetchChanges(ctx: FetchContext): Promise<FetchBatchResult>;
171
+ /**
172
+ * Refuses a read against an object that declares NO read door, instead of letting the base compose a
173
+ * request against the API ROOT. rasa.io has one such object — `Lead Post` is write-only (`APIPath`
174
+ * empty, `CreateAPIPath: /v1/lead-posts`, Create+Update only) — and a caller that syncs "all objects"
175
+ * will ask it to read. Without this guard the empty path resolves to `GET {baseURL}/`, whose failure
176
+ * mode is worse than an error: if the vendor root ever answers 200 (a status/banner document), the
177
+ * envelope reader would treat that payload as this object's record set. Fail precisely and name the
178
+ * cause instead.
179
+ */
180
+ private AssertReadable;
181
+ /** GetRecord also runs NormalizeResponse — publish the same response context first. */
182
+ GetRecord(ctx: GetRecordContext): Promise<ExternalRecord | null>;
183
+ private IsFirstBatch;
184
+ /** Resolves and stores the extraction path + unwrap-verdict state for the object being fetched. */
185
+ private PublishResponseContext;
186
+ /**
187
+ * Composes the LIST path (`ResponseDataKey`) with the verdicted PER-RECORD unwrap path. The unwrap
188
+ * verdict is anchored at the list root, so `results` + `data` → `results[].data`, and
189
+ * `results[].topics` + `data.topics` → `results[].data.topics` (the probe's re-point: the declared
190
+ * fields live under `data`, not at the record root). No verdict ⇒ the declared key verbatim.
191
+ */
192
+ private EffectiveDataPath;
193
+ /**
194
+ * The frozen contract expresses an object's owner as `Configuration.accessPath.parentObject`; the
195
+ * base's template-var resolver reads `Configuration.parentObjectName`. Same declared fact, two
196
+ * spellings — bridged here (in memory only, never persisted) so a `{var}` path resolves its parent
197
+ * instead of returning PARENT_UNRESOLVED. Additive: an existing `parentObjectName` always wins.
198
+ */
199
+ private BridgeAccessPathToParentConfig;
200
+ /** Folds this batch's watermark-field values into the object's running high-water mark. */
201
+ private AdvanceWatermarkHighWater;
202
+ /** Terminal-batch watermark: the run's high-water mark, else the caller's value unchanged. */
203
+ private FinalWatermark;
204
+ /**
205
+ * The generic `CreateRecord` is used as-is; only ID extraction is vendor-shaped. rasa.io returns the
206
+ * new record inside the standard envelope — `PersonApiPostResponse.example` is
207
+ * `{ code, metadata, results: [ { id } ] }` — so the base's root-level `body.id` probe finds nothing.
208
+ * We look at `results[0]` and, when the per-record envelope is in play, `results[0].data`.
209
+ */
210
+ protected ExtractIDFromResponse(response: RESTResponse, idLocation: string | null): string | undefined;
211
+ /** Vendor error envelope: `metadata.errors` alongside the standard `code` / `status_code`. */
212
+ protected ExtractErrorMessage(response: RESTResponse): string | undefined;
213
+ /**
214
+ * Enumerates the object universe from rasa.io's PUBLICLY-PUBLISHED OpenAPI documents (both API
215
+ * generations), unioned with the persisted Declared metadata. Deliberately credential-free: the
216
+ * runtime structure self-check runs without a token, so standard objects MUST re-yield without one.
217
+ * A live credential is additive only (see {@link IntrospectSchema}). If the specs are unreachable the
218
+ * method degrades to the persisted set rather than failing the sync.
219
+ */
220
+ DiscoverObjects(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<ExternalObjectSchema[]>;
221
+ /**
222
+ * Fields for one object: the persisted Declared set, unioned with every property the PUBLIC spec
223
+ * declares on the object's backing definitions (`Configuration.recordSchemas`). Credential-free —
224
+ * a token contributes nothing here. Declared entries win on collision; spec-only properties are
225
+ * appended so a vendor schema addition surfaces without a metadata re-extract.
226
+ */
227
+ DiscoverFields(companyIntegration: MJCompanyIntegrationEntity, objectName: string, contextUser: UserInfo): Promise<ExternalFieldSchema[]>;
228
+ /**
229
+ * Sample-union enrichment (MJ connector standard): after the cache-driven introspection, sample each
230
+ * object's LIVE read shape and union it into the declared field set — this is where a tenant's own
231
+ * custom person attributes reach the schema. Best-effort; a sample failure leaves the declared set
232
+ * untouched. Overrides `IntrospectSchema`, NOT `DiscoverFields` (which would recurse into
233
+ * `DiscoverFieldsViaFetch`'s own fallback).
234
+ *
235
+ * SEQUENTIAL, and that is load-bearing. {@link FetchChanges} publishes its per-object response
236
+ * context onto INSTANCE state (`responseCtx`/`currentWatermark`/`pendingWarnings`) which is read
237
+ * back AFTER the HTTP round-trip. Sampling the catalog with `Promise.all` therefore raced: every
238
+ * concurrent call overwrote `responseCtx`, so a response was normalized against some OTHER object's
239
+ * `DataPath`, matched nothing, and yielded ZERO records — cleanly, with no throw and no warning.
240
+ * Observed live: 17 of 18 objects that reached the sampler logged `rows=0 | cols: []`, the sole
241
+ * survivor being the one that happened to win the race, and the resulting all-null field widths
242
+ * were what made the framework's unknown-width defect drop 8,841 records. The sync engine iterates
243
+ * objects sequentially, which is why the identical read path works there. Do not re-parallelize
244
+ * this without first threading the response context through the call instead of the instance.
245
+ */
246
+ IntrospectSchema(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<SourceSchemaInfo>;
247
+ /** Declared fields, tolerating an object the cache doesn't carry (a spec-only discovery). */
248
+ private SafeDeclaredFields;
249
+ /** The Swagger definition name(s) backing an object, per its declared `Configuration.recordSchemas`. */
250
+ private RecordSchemaNamesFor;
251
+ /** Reverse index: Swagger definition name → the Declared object that claims it. */
252
+ private IndexDeclaredObjectsByRecordSchema;
253
+ /**
254
+ * The set of doors (normalized endpoint paths) already served by a Declared object. Used to suppress
255
+ * spec-only "objects" that are really an alias of a Declared resource — see the door test in
256
+ * {@link DiscoverObjects}. Both the frozen contract's `accessPath.door` and the row's own `APIPath`
257
+ * are indexed, since either may carry the endpoint for a given object.
258
+ */
259
+ private IndexDeclaredDoors;
260
+ /**
261
+ * Canonical form of an endpoint path for identity comparison: version segment dropped (a Declared
262
+ * `APIPath` carries `/v1`|`/v2`; a Swagger 2.0 path key does not — the prefix lives in `basePath`),
263
+ * path parameters collapsed to `{}` (`/persons/{id}/topics` ≡ `/persons/{person_id}/topics`), and
264
+ * casing/trailing slashes normalized.
265
+ */
266
+ private NormalizeDoor;
267
+ /**
268
+ * Every ACTIVE IntegrationObject for this integration, read from the same engine cache the base
269
+ * class reads (`IntegrationEngineBase.GetActiveIntegrationObjects`). Empty when the engine has not
270
+ * been configured yet — discovery then degrades to the spec-only naming, never throws.
271
+ */
272
+ private ActiveObjects;
273
+ /**
274
+ * Fetches + merges rasa.io's public Swagger documents (v1 and v2) with NO credential, and walks
275
+ * every readable operation to enumerate RECORD TYPES (not entry points): each GET's success schema
276
+ * is resolved through the `{code, metadata, results[]}` envelope down to the item definition. Only
277
+ * the item type itself is yielded — nested `$ref`'d sub-objects are NOT promoted to record types
278
+ * (see {@link EnumerateRecordTypes}). Cached for the process lifetime.
279
+ */
280
+ private LoadPublicSpecs;
281
+ private FetchSpec;
282
+ /** Walks every readable operation in one document, yielding the record types it exposes. */
283
+ private EnumerateRecordTypes;
284
+ /** Descends the `{code, metadata, results[]}` envelope to the item definition name. */
285
+ private ResolveRecordDefinitionName;
286
+ private RefName;
287
+ /** Maps a Swagger definition's properties onto ExternalFieldSchema. Constraints come from the spec only. */
288
+ private FieldsFromDefinition;
289
+ /** Human-readable object name for a spec-only record type (`PersonsApiGetResponseItem` → `Persons`). */
290
+ private DeriveObjectName;
291
+ /**
292
+ * Proves the whole auth chain end-to-end: mint the JWT (step 1) and spend it on a read (step 2).
293
+ * Uses `/v1/communities` — the smallest documented readable surface every v1 credential can reach.
294
+ */
295
+ TestConnection(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<ConnectionTestResult>;
296
+ /** Credential record first (the supported path), CompanyIntegration.Configuration as the fallback. */
297
+ private ParseConfig;
298
+ private ParseConfigFromCredential;
299
+ /** Case-insensitive key matching over the credential payload, then strict Zod validation. */
300
+ private NormalizeConfigValues;
301
+ /** Parses an IntegrationObject's Configuration JSON into the typed per-object shape. */
302
+ private ObjectConfig;
303
+ /** Narrows an unknown to a plain object record, or null. */
304
+ private AsRecord;
305
+ }
306
+ export {};