@memberjunction/connector-microsoft-dynamics-365-dataverse 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.
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { type UserInfo } from '@memberjunction/core';
|
|
2
|
+
import type { MJCompanyIntegrationEntity, MJIntegrationObjectEntity, MJIntegrationObjectFieldEntity } 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, type RateLimitPolicy } from '@memberjunction/integration-engine';
|
|
4
|
+
/**
|
|
5
|
+
* Connection configuration parsed from the CompanyIntegration's MJ Credential
|
|
6
|
+
* (preferred) or its Configuration JSON. Dynamics 365 / Dataverse is reached
|
|
7
|
+
* through the OData v4 Web API using a Microsoft Entra ID (Azure AD)
|
|
8
|
+
* client-credentials (server-to-server / app-only) token.
|
|
9
|
+
*
|
|
10
|
+
* Field names are read case-insensitively so a credential authored as
|
|
11
|
+
* `tenantId`/`TenantID`/`tenant_id` all resolve.
|
|
12
|
+
*/
|
|
13
|
+
export interface DynamicsConnectionConfig {
|
|
14
|
+
/** Microsoft Entra ID tenant GUID (or `common`/`organizations`). */
|
|
15
|
+
TenantId: string;
|
|
16
|
+
/** Application (client) registration GUID. */
|
|
17
|
+
ClientId: string;
|
|
18
|
+
/** Application client secret. */
|
|
19
|
+
ClientSecret: string;
|
|
20
|
+
/**
|
|
21
|
+
* The Dataverse environment URL, e.g. `https://contoso.crm.dynamics.com`.
|
|
22
|
+
* Per-customer — NEVER hardcoded. The OAuth resource/audience scope is
|
|
23
|
+
* `<EnvironmentUrl>/.default` and the Web API base is `<EnvironmentUrl>/api/data/v9.2`.
|
|
24
|
+
*/
|
|
25
|
+
EnvironmentUrl: string;
|
|
26
|
+
/** Override the Web API version segment. Default: `v9.2`. */
|
|
27
|
+
ApiVersion?: string;
|
|
28
|
+
/** Override the Entra ID token endpoint origin. Default: `https://login.microsoftonline.com`. */
|
|
29
|
+
AuthorityHost?: string;
|
|
30
|
+
/** Override the OAuth scope. Default: `<EnvironmentUrl>/.default`. */
|
|
31
|
+
Scope?: string;
|
|
32
|
+
/** Maximum retries for rate-limited / transient failures. Default: 5. */
|
|
33
|
+
MaxRetries?: number;
|
|
34
|
+
/** HTTP request timeout in milliseconds. Default: 60000. */
|
|
35
|
+
RequestTimeoutMs?: number;
|
|
36
|
+
/** Minimum milliseconds between API requests. Default: 0 (governed by the engine's adaptive bucket). */
|
|
37
|
+
MinRequestIntervalMs?: number;
|
|
38
|
+
/** Requested OData max page size (capped at 5000 by the service). Default: 5000. */
|
|
39
|
+
MaxPageSize?: number;
|
|
40
|
+
}
|
|
41
|
+
/** Auth context carrying the minted bearer token plus the resolved Web API base URL + config. */
|
|
42
|
+
interface DynamicsAuthContext extends RESTAuthContext {
|
|
43
|
+
Config: DynamicsConnectionConfig;
|
|
44
|
+
/**
|
|
45
|
+
* Org ROOT URL (`<EnvironmentUrl>`) used by the base class's BuildFullURL(baseURL, APIPath).
|
|
46
|
+
* The frozen IO `APIPath`s are ABSOLUTE-from-root (they already include `/api/data/v9.2/...`), so
|
|
47
|
+
* the base must be the org root to avoid doubling the version segment.
|
|
48
|
+
*/
|
|
49
|
+
BaseUrl: string;
|
|
50
|
+
/** Versioned Web API base (`<EnvironmentUrl>/api/data/v9.2`) for the connector's OWN calls (WhoAmI, EntityDefinitions, delta). */
|
|
51
|
+
ApiBaseUrl: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Connector for Microsoft Dynamics 365 / Dataverse (Common Data Service) via the
|
|
55
|
+
* OData v4 Web API.
|
|
56
|
+
*
|
|
57
|
+
* - **Auth**: Microsoft Entra ID OAuth 2.0 client-credentials (app-only). Token is
|
|
58
|
+
* minted/cached by the shared {@link OAuth2TokenManager}; the credential bytes are
|
|
59
|
+
* resolved at runtime from the connection's MJ Credential / Configuration — never baked.
|
|
60
|
+
* - **Base URL**: `<EnvironmentUrl>/api/data/v9.2`, per-connection (every customer
|
|
61
|
+
* environment differs — nothing is hardcoded).
|
|
62
|
+
* - **Discovery**: `DiscoverObjects` / `DiscoverFields` parse the credentialed
|
|
63
|
+
* `EntityDefinitions` describe endpoint at runtime (case-2 auth-gated discovery) so
|
|
64
|
+
* BOTH standard and custom/solution-installed tables surface. The documented standard
|
|
65
|
+
* catalog lives in the Declared metadata file, not in this class.
|
|
66
|
+
* - **Pagination**: OData `@odata.nextLink` cursor, followed verbatim.
|
|
67
|
+
* - **Write**: generic per-operation CRUD from {@link BaseRESTIntegrationConnector}
|
|
68
|
+
* (flat body; create ID read from the `OData-EntityId` response header; update PATCH
|
|
69
|
+
* `/<entityset>(id)`; delete DELETE `/<entityset>(id)`; alternate-key upsert path form).
|
|
70
|
+
* - **Incremental**: change-tracking delta (`Prefer: odata.track-changes` →
|
|
71
|
+
* `@odata.deltaLink`) when a table declares it; `modifiedon`-polling fallback otherwise.
|
|
72
|
+
*/
|
|
73
|
+
export declare class DynamicsDataverseConnector extends BaseRESTIntegrationConnector {
|
|
74
|
+
private readonly tokenManager;
|
|
75
|
+
private cachedAuth;
|
|
76
|
+
/** Verbatim from the upstream identity handoff. Drives the three-way name invariant.
|
|
77
|
+
* Returned as a string literal (not the INTEGRATION_NAME const ref) so the T1
|
|
78
|
+
* ThreeWayName static parser can extract it from source. */
|
|
79
|
+
get IntegrationName(): string;
|
|
80
|
+
get SupportsCreate(): boolean;
|
|
81
|
+
get SupportsUpdate(): boolean;
|
|
82
|
+
get SupportsDelete(): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* §7 — `EntityDefinitions` enumerates the COMPLETE credentialed gamut (standard + custom +
|
|
85
|
+
* solution-installed tables / attributes), not a filtered subset, so a comprehensive refresh
|
|
86
|
+
* may safely + reversibly deactivate objects no longer in the response.
|
|
87
|
+
*/
|
|
88
|
+
get DiscoveryIsAuthoritative(): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* §7 — the watermark this connector advances (delta-link / modifiedon high-water) IS monotonic:
|
|
91
|
+
* change-tracking yields a forward-only delta token, and the modifiedon fallback fetches in
|
|
92
|
+
* ascending `modifiedon` order so the last batch carries the true maximum. Lets the engine narrow
|
|
93
|
+
* the next incremental instead of advancing to wall-clock now.
|
|
94
|
+
*/
|
|
95
|
+
get MonotonicWatermark(): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* §7 — keyset/seek resume key for watermark-less objects: the table's GUID primary key
|
|
98
|
+
* (`<logicalName>id`), read from the IO's Configuration. Stable + monotonic-enough for
|
|
99
|
+
* resume-from-last-seen; `null` when no PK is known.
|
|
100
|
+
*/
|
|
101
|
+
StableOrderingKey(objectName: string): string | null;
|
|
102
|
+
/**
|
|
103
|
+
* §7 — Dataverse enforces a per-environment Web API service-protection limit (≈ 6000 requests /
|
|
104
|
+
* 300s sliding window, plus an execution-time budget). Conservative sustained rate keeps the
|
|
105
|
+
* connector under the request-count limit; the engine's AIMD bucket adapts on 429s via
|
|
106
|
+
* {@link ExtractRetryAfterMs}. (≈6000/300 ≈ 20 req/s; we stay below to leave headroom.)
|
|
107
|
+
*/
|
|
108
|
+
get RateLimitPolicy(): RateLimitPolicy | null;
|
|
109
|
+
get MaxConcurrencyHint(): number | null;
|
|
110
|
+
/**
|
|
111
|
+
* Parse Dataverse's throttle signal. The service returns HTTP 429 with a `Retry-After` header
|
|
112
|
+
* (seconds) on a service-protection limit; honor it precisely.
|
|
113
|
+
*/
|
|
114
|
+
ExtractRetryAfterMs(error: unknown): number | undefined;
|
|
115
|
+
/**
|
|
116
|
+
* Validates the service principal can reach the environment by issuing a fresh token and a
|
|
117
|
+
* lightweight `WhoAmI` call (an unbound function returning the calling user/org GUIDs).
|
|
118
|
+
*/
|
|
119
|
+
TestConnection(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<ConnectionTestResult>;
|
|
120
|
+
/**
|
|
121
|
+
* Case-2 (auth-gated) runtime discovery: enumerates EVERY table the credential can see via the
|
|
122
|
+
* `EntityDefinitions` describe endpoint (standard + custom + solution-installed) — NOT a baked
|
|
123
|
+
* catalog. The documented standard catalog seeded in the Declared metadata file is the floor;
|
|
124
|
+
* this returns what the live environment actually exposes (the ceiling), which is strictly a
|
|
125
|
+
* superset for custom-bearing tenants.
|
|
126
|
+
*/
|
|
127
|
+
DiscoverObjects(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<ExternalObjectSchema[]>;
|
|
128
|
+
/**
|
|
129
|
+
* Case-2 runtime discovery for a single object's columns: describes the table's `Attributes`
|
|
130
|
+
* collection and maps each Property → {@link ExternalFieldSchema}, surfacing Type/MaxLength,
|
|
131
|
+
* the GUID PK (`IsPrimaryId` / `PrimaryIdAttribute` → `IsPrimaryKey`), and scalar Lookup
|
|
132
|
+
* columns → FK (with `ForeignKeyTarget` = the referenced table's logical name).
|
|
133
|
+
*/
|
|
134
|
+
DiscoverFields(companyIntegration: MJCompanyIntegrationEntity, objectName: string, contextUser: UserInfo): Promise<ExternalFieldSchema[]>;
|
|
135
|
+
/**
|
|
136
|
+
* Routes to the change-tracking delta path when the table declares it AND a watermark exists;
|
|
137
|
+
* otherwise delegates to the base metadata-driven fetch (which applies the `modifiedon` watermark
|
|
138
|
+
* fallback + standard `@odata.nextLink` pagination). The base path already handles
|
|
139
|
+
* `TransformRecord` (annotation stripping) + full-record pass-through, so the delta path mirrors it.
|
|
140
|
+
*/
|
|
141
|
+
FetchChanges(ctx: FetchContext): Promise<FetchBatchResult>;
|
|
142
|
+
/**
|
|
143
|
+
* Removes the OData control annotations (`@odata.etag`, `@odata.context`, `@odata.id`,
|
|
144
|
+
* `<lookup>@odata.bind`, `<lookup>@OData.Community.Display.V1.FormattedValue`, etc.) Dataverse
|
|
145
|
+
* sprinkles onto every record — they are response transport metadata, not columns. Every other
|
|
146
|
+
* source key is preserved (full-record pass-through); the removed keys are declared in
|
|
147
|
+
* {@link ExcludedSourceKeys} so the base re-add doesn't restore them and change-detection ignores them.
|
|
148
|
+
*/
|
|
149
|
+
protected TransformRecord(raw: Record<string, unknown>, _obj: MJIntegrationObjectEntity, _fields: MJIntegrationObjectFieldEntity[]): Record<string, unknown>;
|
|
150
|
+
/**
|
|
151
|
+
* The well-known static OData control annotation keys {@link TransformRecord} drops. The base
|
|
152
|
+
* `applyTransformPreservingKeys` uses this to avoid re-adding them. Dataverse ALSO emits dynamic,
|
|
153
|
+
* per-lookup annotation suffixes (`<col>@odata.bind`, `<col>@OData.Community.Display.V1.FormattedValue`,
|
|
154
|
+
* `<col>@Microsoft.Dynamics.CRM.lookuplogicalname`) whose exact names vary by record and cannot be
|
|
155
|
+
* enumerated statically — those are handled by the {@link applyTransformPreservingKeys} override
|
|
156
|
+
* below (predicate exclusion), so they never re-appear. This static list covers the always-present
|
|
157
|
+
* envelope keys for completeness / auditability.
|
|
158
|
+
*/
|
|
159
|
+
protected ExcludedSourceKeys(_objectName: string): string[];
|
|
160
|
+
/**
|
|
161
|
+
* Overrides the base re-add so the DYNAMIC OData annotation keys (`<col>@odata.bind`, FormattedValue,
|
|
162
|
+
* lookuplogicalname, etc.) {@link TransformRecord} strips are NOT restored. The base loop re-adds any
|
|
163
|
+
* `raw` key absent from the transform output unless it is in the static {@link ExcludedSourceKeys}
|
|
164
|
+
* set — which can't enumerate the per-record annotation suffixes. Here we re-add a dropped key only
|
|
165
|
+
* when it is neither statically excluded NOR an annotation key (by predicate). This keeps full-record
|
|
166
|
+
* pass-through for genuine columns while making the annotation removal stick (auditable, by rule).
|
|
167
|
+
*/
|
|
168
|
+
protected applyTransformPreservingKeys(raw: Record<string, unknown>, obj: MJIntegrationObjectEntity, fields: MJIntegrationObjectFieldEntity[]): Record<string, unknown>;
|
|
169
|
+
/**
|
|
170
|
+
* Mints/caches a Microsoft Entra ID access token via client-credentials. The scope is the
|
|
171
|
+
* environment's `.default` (`<EnvironmentUrl>/.default`) so the token's audience matches the
|
|
172
|
+
* Dataverse resource. Token is cached by {@link OAuth2TokenManager} until near expiry.
|
|
173
|
+
*/
|
|
174
|
+
protected Authenticate(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo, forceRefresh?: boolean): Promise<DynamicsAuthContext>;
|
|
175
|
+
/** Runs the client_credentials token round-trip through {@link OAuth2TokenManager}. */
|
|
176
|
+
private MintToken;
|
|
177
|
+
/** Standard OData/Dataverse headers — bearer token + JSON + OData version + return-representation. */
|
|
178
|
+
protected BuildHeaders(auth: RESTAuthContext): Record<string, string>;
|
|
179
|
+
/**
|
|
180
|
+
* HTTP transport with retry/backoff on 429/503/504 (Dataverse service-protection limits). The
|
|
181
|
+
* thrown error on a retryable status carries the response headers so {@link ExtractRetryAfterMs}
|
|
182
|
+
* can read `Retry-After` for the engine's adaptive bucket.
|
|
183
|
+
*/
|
|
184
|
+
protected MakeHTTPRequest(auth: RESTAuthContext, url: string, method: string, headers: Record<string, string>, body?: unknown): Promise<RESTResponse>;
|
|
185
|
+
/**
|
|
186
|
+
* Unwraps the OData collection envelope's `value` array. A single-record GET (`/<set>(id)`) is
|
|
187
|
+
* returned as a one-element array. Honors a metadata-declared `ResponseDataKey` if set, defaulting
|
|
188
|
+
* to the OData `value` convention.
|
|
189
|
+
*/
|
|
190
|
+
protected NormalizeResponse(rawBody: unknown, responseDataKey: string | null): Record<string, unknown>[];
|
|
191
|
+
/**
|
|
192
|
+
* Cursor pagination via `@odata.nextLink`. The nextLink is an ABSOLUTE continuation URL that
|
|
193
|
+
* encodes the exact next request (including the opaque `$skiptoken`); it is followed VERBATIM and
|
|
194
|
+
* never hand-built. A present nextLink means another page exists.
|
|
195
|
+
*/
|
|
196
|
+
protected ExtractPaginationInfo(rawBody: unknown, _paginationType: PaginationType, _currentPage: number, _currentOffset: number, _pageSize: number): PaginationState;
|
|
197
|
+
/**
|
|
198
|
+
* Per-connection base the engine joins the IO `APIPath` onto. The frozen Dataverse IO APIPaths are
|
|
199
|
+
* ABSOLUTE-from-root (they already include `/api/data/v9.2/...`), so this returns the org ROOT
|
|
200
|
+
* (`<EnvironmentUrl>`) and the resolved request URL is `<EnvironmentUrl>/api/data/v9.2/<entityset>`.
|
|
201
|
+
* The versioned Web API base (`<EnvironmentUrl>/api/data/v9.2`) is `ApiBaseUrl`, used by the
|
|
202
|
+
* connector's OWN calls (WhoAmI / EntityDefinitions / delta). Never a hardcoded org URL.
|
|
203
|
+
*/
|
|
204
|
+
protected GetBaseURL(_companyIntegration: MJCompanyIntegrationEntity, auth: RESTAuthContext): string;
|
|
205
|
+
/**
|
|
206
|
+
* Honors the OData `@odata.nextLink` cursor (used verbatim) and otherwise injects the page-size
|
|
207
|
+
* request as a `$top`-free first page — Dataverse caps page size via the `Prefer: odata.maxpagesize`
|
|
208
|
+
* header (added in {@link MakeFirstPageHeaders}), NOT a query param, and does NOT support `$skip`.
|
|
209
|
+
*/
|
|
210
|
+
protected BuildPaginatedURL(basePath: string, _obj: MJIntegrationObjectEntity, _page: number, _offset: number, cursor?: string, _effectivePageSize?: number): string;
|
|
211
|
+
/**
|
|
212
|
+
* Dataverse returns the created record's URI in the `OData-EntityId` response header
|
|
213
|
+
* (e.g. `https://org.crm.dynamics.com/api/data/v9.2/accounts(00000000-...)`); the GUID is the
|
|
214
|
+
* trailing `(...)` segment. Falls back to the `Location` header, then the body PK, so a tenant
|
|
215
|
+
* configured to return-representation still resolves. An empty ID makes the create FAIL LOUDLY
|
|
216
|
+
* via the base's BuildCreatedResult (never a silent duplicate-create).
|
|
217
|
+
*/
|
|
218
|
+
protected ExtractIDFromResponse(response: RESTResponse, idLocation: string | null): string | undefined;
|
|
219
|
+
/**
|
|
220
|
+
* Substitutes the target ID into a Dataverse single-record path. Supports the standard GUID form
|
|
221
|
+
* `/<entityset>(id)` AND the alternate-key upsert form `/<entityset>(<altkey>='<value>')` — when
|
|
222
|
+
* the ExternalID already contains an `=` it is an alternate-key expression and is inserted as-is
|
|
223
|
+
* (only the value is escaped); otherwise it is treated as a GUID. Handles both `({id})` template
|
|
224
|
+
* placeholders and a bare `(id)` suffix.
|
|
225
|
+
*/
|
|
226
|
+
protected SubstituteIDInPath(path: string, externalID: string, idLocation: string | null): string;
|
|
227
|
+
/**
|
|
228
|
+
* Builds the OData key expression for a single-record path. A plain GUID → `00000000-...`. An
|
|
229
|
+
* alternate-key expression (`key='value'` or `k1='v1',k2='v2'`) is passed through with each value
|
|
230
|
+
* single-quote-escaped. This realizes the alternate-key upsert path form the metadata may declare.
|
|
231
|
+
*/
|
|
232
|
+
private BuildKeyExpression;
|
|
233
|
+
/** Whether a table is configured for change tracking (Configuration.changeTrackingHeader present). */
|
|
234
|
+
private ObjectUsesChangeTracking;
|
|
235
|
+
/**
|
|
236
|
+
* Fetches changed records via Dataverse change tracking. The watermark holds the previous poll's
|
|
237
|
+
* `@odata.deltaLink` (a full URL carrying `$deltatoken`) — followed VERBATIM. Deleted records
|
|
238
|
+
* arrive as `$deletedEntity` references; they are surfaced with `IsDeleted=true`. The last page's
|
|
239
|
+
* `@odata.deltaLink` becomes the new watermark for the next poll.
|
|
240
|
+
*/
|
|
241
|
+
private FetchChangesViaDelta;
|
|
242
|
+
/** Maps one change-tracking row (live record OR `$deletedEntity` tombstone) to an ExternalRecord. */
|
|
243
|
+
private DeltaRowToExternalRecord;
|
|
244
|
+
/** Fetches the full table list via `EntityDefinitions` (paged, following @odata.nextLink). */
|
|
245
|
+
private FetchEntityDefinitions;
|
|
246
|
+
/** Describes one table + its Attributes by logical name. Returns null if not visible to the credential. */
|
|
247
|
+
private FetchEntityDefinition;
|
|
248
|
+
/** Resolves the Dataverse logical name for an IO (Configuration.logicalName, else the IO name). */
|
|
249
|
+
private ResolveLogicalName;
|
|
250
|
+
/** Maps a Dataverse table describe entry → ExternalObjectSchema. */
|
|
251
|
+
private EntityMetadataToObjectSchema;
|
|
252
|
+
/** Maps a Dataverse attribute describe entry → ExternalFieldSchema. */
|
|
253
|
+
private AttributeMetadataToFieldSchema;
|
|
254
|
+
/** Reads a Dataverse localized label, falling back to undefined. */
|
|
255
|
+
private LocalizedLabel;
|
|
256
|
+
/** Parses the connection config, preferring the attached MJ Credential over Configuration JSON. */
|
|
257
|
+
private ParseConfig;
|
|
258
|
+
/** Loads the OAuth2 config from the MJ: Credentials entity Values JSON. */
|
|
259
|
+
private ParseConfigFromCredential;
|
|
260
|
+
/** Validates the parsed config + applies defaults. Field names are read case-insensitively. */
|
|
261
|
+
private ValidateConfig;
|
|
262
|
+
/** Resolves the Web API base URL: `<EnvironmentUrl>/api/data/v9.2`. */
|
|
263
|
+
private ResolveBaseUrl;
|
|
264
|
+
private ExecuteOneRequest;
|
|
265
|
+
private ParseResponseBody;
|
|
266
|
+
private ExtractHeaders;
|
|
267
|
+
private IsRetryable;
|
|
268
|
+
private ComputeBackoffDelay;
|
|
269
|
+
private Sleep;
|
|
270
|
+
/**
|
|
271
|
+
* True for any OData/Dataverse annotation key. Genuine columns never contain `@`; every annotation
|
|
272
|
+
* does — the response-level controls (`@odata.etag`, `@odata.context`, …) and the per-column
|
|
273
|
+
* suffixes (`<col>@odata.bind`, `<col>@OData.Community.Display.V1.FormattedValue`,
|
|
274
|
+
* `<col>@Microsoft.Dynamics.CRM.lookuplogicalname`). So an `@`-containing key is an annotation.
|
|
275
|
+
*/
|
|
276
|
+
private IsODataAnnotationKey;
|
|
277
|
+
private buildDataverseURL;
|
|
278
|
+
private previewBody;
|
|
279
|
+
/** Integration ID of the most recent fetch — lets the no-arg StableOrderingKey hook locate the IO. */
|
|
280
|
+
private lastIntegrationID;
|
|
281
|
+
/**
|
|
282
|
+
* Best-effort cached-object lookup by NAME using the last-seen integration ID. Used by
|
|
283
|
+
* {@link StableOrderingKey}, which the engine calls without a CompanyIntegration; returns null
|
|
284
|
+
* (keyset resume simply unavailable) before any fetch has run.
|
|
285
|
+
*/
|
|
286
|
+
private tryGetCachedObject;
|
|
287
|
+
private tryGetCachedObjectFor;
|
|
288
|
+
/** Reads a trimmed string from an IO's Configuration JSON, tolerant of absent/invalid. */
|
|
289
|
+
private readObjectConfigString;
|
|
290
|
+
/** Extracts response headers from a thrown error, when the connector throws an error carrying them. */
|
|
291
|
+
private extractHeadersFromError;
|
|
292
|
+
}
|
|
293
|
+
/** Tree-shaking prevention function — import and call from the package entry point. */
|
|
294
|
+
export declare function LoadDynamicsDataverseConnector(): void;
|
|
295
|
+
export {};
|
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
8
|
+
import { Metadata } from '@memberjunction/core';
|
|
9
|
+
import { BaseIntegrationConnector, BaseRESTIntegrationConnector, OAuth2TokenManager, } from '@memberjunction/integration-engine';
|
|
10
|
+
// ─── Constants ───────────────────────────────────────────────────────
|
|
11
|
+
/** Default Web API version segment. */
|
|
12
|
+
const DEFAULT_API_VERSION = 'v9.2';
|
|
13
|
+
/** Default Entra ID authority host. */
|
|
14
|
+
const DEFAULT_AUTHORITY_HOST = 'https://login.microsoftonline.com';
|
|
15
|
+
/** Default HTTP request timeout (Dataverse can be slow under load). */
|
|
16
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
17
|
+
/** Default retry count for retryable (429 / 503 / 504) responses. */
|
|
18
|
+
const DEFAULT_MAX_RETRIES = 5;
|
|
19
|
+
/** OData service hard cap on page size; the service silently ignores requests above this. */
|
|
20
|
+
const MAX_ODATA_PAGE_SIZE = 5_000;
|
|
21
|
+
/**
|
|
22
|
+
* Connector for Microsoft Dynamics 365 / Dataverse (Common Data Service) via the
|
|
23
|
+
* OData v4 Web API.
|
|
24
|
+
*
|
|
25
|
+
* - **Auth**: Microsoft Entra ID OAuth 2.0 client-credentials (app-only). Token is
|
|
26
|
+
* minted/cached by the shared {@link OAuth2TokenManager}; the credential bytes are
|
|
27
|
+
* resolved at runtime from the connection's MJ Credential / Configuration — never baked.
|
|
28
|
+
* - **Base URL**: `<EnvironmentUrl>/api/data/v9.2`, per-connection (every customer
|
|
29
|
+
* environment differs — nothing is hardcoded).
|
|
30
|
+
* - **Discovery**: `DiscoverObjects` / `DiscoverFields` parse the credentialed
|
|
31
|
+
* `EntityDefinitions` describe endpoint at runtime (case-2 auth-gated discovery) so
|
|
32
|
+
* BOTH standard and custom/solution-installed tables surface. The documented standard
|
|
33
|
+
* catalog lives in the Declared metadata file, not in this class.
|
|
34
|
+
* - **Pagination**: OData `@odata.nextLink` cursor, followed verbatim.
|
|
35
|
+
* - **Write**: generic per-operation CRUD from {@link BaseRESTIntegrationConnector}
|
|
36
|
+
* (flat body; create ID read from the `OData-EntityId` response header; update PATCH
|
|
37
|
+
* `/<entityset>(id)`; delete DELETE `/<entityset>(id)`; alternate-key upsert path form).
|
|
38
|
+
* - **Incremental**: change-tracking delta (`Prefer: odata.track-changes` →
|
|
39
|
+
* `@odata.deltaLink`) when a table declares it; `modifiedon`-polling fallback otherwise.
|
|
40
|
+
*/
|
|
41
|
+
let DynamicsDataverseConnector = class DynamicsDataverseConnector extends BaseRESTIntegrationConnector {
|
|
42
|
+
constructor() {
|
|
43
|
+
// ── State ────────────────────────────────────────────────────────
|
|
44
|
+
super(...arguments);
|
|
45
|
+
this.tokenManager = new OAuth2TokenManager();
|
|
46
|
+
this.cachedAuth = null;
|
|
47
|
+
/** Integration ID of the most recent fetch — lets the no-arg StableOrderingKey hook locate the IO. */
|
|
48
|
+
this.lastIntegrationID = null;
|
|
49
|
+
}
|
|
50
|
+
// ── Identity + capabilities ──────────────────────────────────────
|
|
51
|
+
/** Verbatim from the upstream identity handoff. Drives the three-way name invariant.
|
|
52
|
+
* Returned as a string literal (not the INTEGRATION_NAME const ref) so the T1
|
|
53
|
+
* ThreeWayName static parser can extract it from source. */
|
|
54
|
+
get IntegrationName() { return 'Microsoft Dynamics 365 (Dataverse)'; }
|
|
55
|
+
get SupportsCreate() { return true; }
|
|
56
|
+
get SupportsUpdate() { return true; }
|
|
57
|
+
get SupportsDelete() { return true; }
|
|
58
|
+
/**
|
|
59
|
+
* §7 — `EntityDefinitions` enumerates the COMPLETE credentialed gamut (standard + custom +
|
|
60
|
+
* solution-installed tables / attributes), not a filtered subset, so a comprehensive refresh
|
|
61
|
+
* may safely + reversibly deactivate objects no longer in the response.
|
|
62
|
+
*/
|
|
63
|
+
get DiscoveryIsAuthoritative() { return true; }
|
|
64
|
+
/**
|
|
65
|
+
* §7 — the watermark this connector advances (delta-link / modifiedon high-water) IS monotonic:
|
|
66
|
+
* change-tracking yields a forward-only delta token, and the modifiedon fallback fetches in
|
|
67
|
+
* ascending `modifiedon` order so the last batch carries the true maximum. Lets the engine narrow
|
|
68
|
+
* the next incremental instead of advancing to wall-clock now.
|
|
69
|
+
*/
|
|
70
|
+
get MonotonicWatermark() { return true; }
|
|
71
|
+
/**
|
|
72
|
+
* §7 — keyset/seek resume key for watermark-less objects: the table's GUID primary key
|
|
73
|
+
* (`<logicalName>id`), read from the IO's Configuration. Stable + monotonic-enough for
|
|
74
|
+
* resume-from-last-seen; `null` when no PK is known.
|
|
75
|
+
*/
|
|
76
|
+
StableOrderingKey(objectName) {
|
|
77
|
+
const obj = this.tryGetCachedObject(objectName);
|
|
78
|
+
if (!obj)
|
|
79
|
+
return null;
|
|
80
|
+
return this.readObjectConfigString(obj, 'primaryIdAttribute')
|
|
81
|
+
?? this.readObjectConfigString(obj, 'stableOrderingKey');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* §7 — Dataverse enforces a per-environment Web API service-protection limit (≈ 6000 requests /
|
|
85
|
+
* 300s sliding window, plus an execution-time budget). Conservative sustained rate keeps the
|
|
86
|
+
* connector under the request-count limit; the engine's AIMD bucket adapts on 429s via
|
|
87
|
+
* {@link ExtractRetryAfterMs}. (≈6000/300 ≈ 20 req/s; we stay below to leave headroom.)
|
|
88
|
+
*/
|
|
89
|
+
get RateLimitPolicy() {
|
|
90
|
+
return { TokensPerSec: 15, Burst: 30, ThrottleBackoffFactor: 0.5 };
|
|
91
|
+
}
|
|
92
|
+
get MaxConcurrencyHint() { return 4; }
|
|
93
|
+
/**
|
|
94
|
+
* Parse Dataverse's throttle signal. The service returns HTTP 429 with a `Retry-After` header
|
|
95
|
+
* (seconds) on a service-protection limit; honor it precisely.
|
|
96
|
+
*/
|
|
97
|
+
ExtractRetryAfterMs(error) {
|
|
98
|
+
const headers = this.extractHeadersFromError(error);
|
|
99
|
+
if (!headers)
|
|
100
|
+
return undefined;
|
|
101
|
+
const retryAfter = headers['retry-after'];
|
|
102
|
+
if (retryAfter) {
|
|
103
|
+
const asSeconds = Number(retryAfter);
|
|
104
|
+
if (!Number.isNaN(asSeconds))
|
|
105
|
+
return Math.max(0, asSeconds * 1_000);
|
|
106
|
+
const asDate = new Date(retryAfter).getTime();
|
|
107
|
+
if (!Number.isNaN(asDate))
|
|
108
|
+
return Math.max(0, asDate - Date.now());
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
// ── TestConnection ───────────────────────────────────────────────
|
|
113
|
+
/**
|
|
114
|
+
* Validates the service principal can reach the environment by issuing a fresh token and a
|
|
115
|
+
* lightweight `WhoAmI` call (an unbound function returning the calling user/org GUIDs).
|
|
116
|
+
*/
|
|
117
|
+
async TestConnection(companyIntegration, contextUser) {
|
|
118
|
+
try {
|
|
119
|
+
const auth = await this.Authenticate(companyIntegration, contextUser, true);
|
|
120
|
+
const url = `${auth.ApiBaseUrl}/WhoAmI`;
|
|
121
|
+
const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
|
|
122
|
+
if (response.Status < 200 || response.Status >= 300) {
|
|
123
|
+
return {
|
|
124
|
+
Success: false,
|
|
125
|
+
Message: `Dataverse WhoAmI failed: HTTP ${response.Status} — ${this.previewBody(response.Body)}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const body = response.Body;
|
|
129
|
+
return {
|
|
130
|
+
Success: true,
|
|
131
|
+
Message: `Connected to Dataverse (org ${body.OrganizationId ?? 'unknown'})`,
|
|
132
|
+
ServerVersion: `Web API ${auth.Config.ApiVersion ?? DEFAULT_API_VERSION}`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
137
|
+
return { Success: false, Message: `Connection failed: ${message}` };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// ── DiscoverObjects / DiscoverFields (runtime $metadata discovery) ──
|
|
141
|
+
/**
|
|
142
|
+
* Case-2 (auth-gated) runtime discovery: enumerates EVERY table the credential can see via the
|
|
143
|
+
* `EntityDefinitions` describe endpoint (standard + custom + solution-installed) — NOT a baked
|
|
144
|
+
* catalog. The documented standard catalog seeded in the Declared metadata file is the floor;
|
|
145
|
+
* this returns what the live environment actually exposes (the ceiling), which is strictly a
|
|
146
|
+
* superset for custom-bearing tenants.
|
|
147
|
+
*/
|
|
148
|
+
async DiscoverObjects(companyIntegration, contextUser) {
|
|
149
|
+
try {
|
|
150
|
+
const entities = await this.FetchEntityDefinitions(companyIntegration, contextUser);
|
|
151
|
+
return entities
|
|
152
|
+
.filter(e => typeof e.LogicalName === 'string' && typeof e.EntitySetName === 'string' && e.EntitySetName.length > 0)
|
|
153
|
+
.map(e => this.EntityMetadataToObjectSchema(e));
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
// Case-2 (auth-gated) discovery: the live EntityDefinitions endpoint requires credentials.
|
|
157
|
+
// When the config/credential is absent, re-throw with an EXPLICIT credential-absence signal
|
|
158
|
+
// so a credential-free run is classified as "discovery requires credentials" (cross-pass
|
|
159
|
+
// consistency is proven at the live tier) rather than a hard discovery failure. A genuine
|
|
160
|
+
// live API error (credentials present) keeps its original message and propagates.
|
|
161
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
162
|
+
if (/CredentialID or Configuration JSON|missing required field|could not be loaded|not a valid object/i.test(msg)) {
|
|
163
|
+
throw new Error(`DynamicsDataverseConnector requires credentials to discover (auth-gated EntityDefinitions endpoint): ${msg}`);
|
|
164
|
+
}
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Case-2 runtime discovery for a single object's columns: describes the table's `Attributes`
|
|
170
|
+
* collection and maps each Property → {@link ExternalFieldSchema}, surfacing Type/MaxLength,
|
|
171
|
+
* the GUID PK (`IsPrimaryId` / `PrimaryIdAttribute` → `IsPrimaryKey`), and scalar Lookup
|
|
172
|
+
* columns → FK (with `ForeignKeyTarget` = the referenced table's logical name).
|
|
173
|
+
*/
|
|
174
|
+
async DiscoverFields(companyIntegration, objectName, contextUser) {
|
|
175
|
+
const entity = await this.FetchEntityDefinition(companyIntegration, objectName, contextUser);
|
|
176
|
+
if (!entity) {
|
|
177
|
+
// Fall back to the Declared metadata cache if the live describe returned nothing
|
|
178
|
+
// (object not visible to these credentials) — degrade gracefully rather than drop it.
|
|
179
|
+
return super.DiscoverFields(companyIntegration, objectName, contextUser);
|
|
180
|
+
}
|
|
181
|
+
const primaryId = entity.PrimaryIdAttribute ?? `${entity.LogicalName}id`;
|
|
182
|
+
const attributes = entity.Attributes ?? [];
|
|
183
|
+
return attributes
|
|
184
|
+
.filter(a => typeof a.LogicalName === 'string' && a.LogicalName.length > 0)
|
|
185
|
+
.map(a => this.AttributeMetadataToFieldSchema(a, primaryId));
|
|
186
|
+
}
|
|
187
|
+
// ── FetchChanges (change-tracking delta override) ────────────────
|
|
188
|
+
/**
|
|
189
|
+
* Routes to the change-tracking delta path when the table declares it AND a watermark exists;
|
|
190
|
+
* otherwise delegates to the base metadata-driven fetch (which applies the `modifiedon` watermark
|
|
191
|
+
* fallback + standard `@odata.nextLink` pagination). The base path already handles
|
|
192
|
+
* `TransformRecord` (annotation stripping) + full-record pass-through, so the delta path mirrors it.
|
|
193
|
+
*/
|
|
194
|
+
async FetchChanges(ctx) {
|
|
195
|
+
this.lastIntegrationID = ctx.CompanyIntegration.IntegrationID;
|
|
196
|
+
const obj = this.GetCachedObject(ctx.CompanyIntegration.IntegrationID, ctx.ObjectName);
|
|
197
|
+
if (obj.SupportsIncrementalSync && this.ObjectUsesChangeTracking(obj) && ctx.WatermarkValue) {
|
|
198
|
+
return this.FetchChangesViaDelta(ctx, obj);
|
|
199
|
+
}
|
|
200
|
+
return super.FetchChanges(ctx);
|
|
201
|
+
}
|
|
202
|
+
// ── TransformRecord — strip OData annotations (auditable removal) ──
|
|
203
|
+
/**
|
|
204
|
+
* Removes the OData control annotations (`@odata.etag`, `@odata.context`, `@odata.id`,
|
|
205
|
+
* `<lookup>@odata.bind`, `<lookup>@OData.Community.Display.V1.FormattedValue`, etc.) Dataverse
|
|
206
|
+
* sprinkles onto every record — they are response transport metadata, not columns. Every other
|
|
207
|
+
* source key is preserved (full-record pass-through); the removed keys are declared in
|
|
208
|
+
* {@link ExcludedSourceKeys} so the base re-add doesn't restore them and change-detection ignores them.
|
|
209
|
+
*/
|
|
210
|
+
TransformRecord(raw, _obj, _fields) {
|
|
211
|
+
const out = {};
|
|
212
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
213
|
+
if (this.IsODataAnnotationKey(key))
|
|
214
|
+
continue;
|
|
215
|
+
out[key] = value;
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The well-known static OData control annotation keys {@link TransformRecord} drops. The base
|
|
221
|
+
* `applyTransformPreservingKeys` uses this to avoid re-adding them. Dataverse ALSO emits dynamic,
|
|
222
|
+
* per-lookup annotation suffixes (`<col>@odata.bind`, `<col>@OData.Community.Display.V1.FormattedValue`,
|
|
223
|
+
* `<col>@Microsoft.Dynamics.CRM.lookuplogicalname`) whose exact names vary by record and cannot be
|
|
224
|
+
* enumerated statically — those are handled by the {@link applyTransformPreservingKeys} override
|
|
225
|
+
* below (predicate exclusion), so they never re-appear. This static list covers the always-present
|
|
226
|
+
* envelope keys for completeness / auditability.
|
|
227
|
+
*/
|
|
228
|
+
ExcludedSourceKeys(_objectName) {
|
|
229
|
+
return ['@odata.etag', '@odata.context', '@odata.id', '@odata.editLink', '@odata.type'];
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Overrides the base re-add so the DYNAMIC OData annotation keys (`<col>@odata.bind`, FormattedValue,
|
|
233
|
+
* lookuplogicalname, etc.) {@link TransformRecord} strips are NOT restored. The base loop re-adds any
|
|
234
|
+
* `raw` key absent from the transform output unless it is in the static {@link ExcludedSourceKeys}
|
|
235
|
+
* set — which can't enumerate the per-record annotation suffixes. Here we re-add a dropped key only
|
|
236
|
+
* when it is neither statically excluded NOR an annotation key (by predicate). This keeps full-record
|
|
237
|
+
* pass-through for genuine columns while making the annotation removal stick (auditable, by rule).
|
|
238
|
+
*/
|
|
239
|
+
applyTransformPreservingKeys(raw, obj, fields) {
|
|
240
|
+
const transformed = this.TransformRecord(raw, obj, fields);
|
|
241
|
+
if (transformed === raw)
|
|
242
|
+
return raw;
|
|
243
|
+
const excluded = new Set(this.ExcludedSourceKeys(obj.Name));
|
|
244
|
+
const out = { ...transformed };
|
|
245
|
+
for (const key of Object.keys(raw)) {
|
|
246
|
+
if (key in out || excluded.has(key) || this.IsODataAnnotationKey(key))
|
|
247
|
+
continue;
|
|
248
|
+
out[key] = raw[key];
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
// ─── BaseRESTIntegrationConnector abstract hooks ────────────────
|
|
253
|
+
/**
|
|
254
|
+
* Mints/caches a Microsoft Entra ID access token via client-credentials. The scope is the
|
|
255
|
+
* environment's `.default` (`<EnvironmentUrl>/.default`) so the token's audience matches the
|
|
256
|
+
* Dataverse resource. Token is cached by {@link OAuth2TokenManager} until near expiry.
|
|
257
|
+
*/
|
|
258
|
+
async Authenticate(companyIntegration, contextUser, forceRefresh = false) {
|
|
259
|
+
if (forceRefresh) {
|
|
260
|
+
this.cachedAuth = null;
|
|
261
|
+
this.tokenManager.Reset();
|
|
262
|
+
}
|
|
263
|
+
else if (this.cachedAuth) {
|
|
264
|
+
// Re-mint through the manager (cheap when cached) so an expired token refreshes.
|
|
265
|
+
const refreshed = await this.MintToken(this.cachedAuth.Config);
|
|
266
|
+
return { ...this.cachedAuth, Token: refreshed };
|
|
267
|
+
}
|
|
268
|
+
const config = await this.ParseConfig(companyIntegration, contextUser);
|
|
269
|
+
const token = await this.MintToken(config);
|
|
270
|
+
const auth = {
|
|
271
|
+
Token: token,
|
|
272
|
+
Config: config,
|
|
273
|
+
BaseUrl: config.EnvironmentUrl.replace(/\/+$/, ''), // org root — IO APIPaths are absolute-from-root
|
|
274
|
+
ApiBaseUrl: this.ResolveBaseUrl(config), // versioned base for the connector's own calls
|
|
275
|
+
};
|
|
276
|
+
this.cachedAuth = auth;
|
|
277
|
+
return auth;
|
|
278
|
+
}
|
|
279
|
+
/** Runs the client_credentials token round-trip through {@link OAuth2TokenManager}. */
|
|
280
|
+
async MintToken(config) {
|
|
281
|
+
const authorityHost = (config.AuthorityHost ?? DEFAULT_AUTHORITY_HOST).replace(/\/+$/, '');
|
|
282
|
+
const tokenURL = `${authorityHost}/${encodeURIComponent(config.TenantId)}/oauth2/v2.0/token`;
|
|
283
|
+
const scope = config.Scope ?? `${config.EnvironmentUrl.replace(/\/+$/, '')}/.default`;
|
|
284
|
+
const req = {
|
|
285
|
+
TokenURL: tokenURL,
|
|
286
|
+
ClientId: config.ClientId,
|
|
287
|
+
ClientSecret: config.ClientSecret,
|
|
288
|
+
// Entra ID v2.0 accepts client_id/client_secret in the form body for confidential clients.
|
|
289
|
+
Scopes: scope,
|
|
290
|
+
ScopeParam: 'scope',
|
|
291
|
+
TimeoutMs: config.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
292
|
+
};
|
|
293
|
+
const token = await this.tokenManager.GetAccessToken(req, 'client_credentials');
|
|
294
|
+
return token.AccessToken;
|
|
295
|
+
}
|
|
296
|
+
/** Standard OData/Dataverse headers — bearer token + JSON + OData version + return-representation. */
|
|
297
|
+
BuildHeaders(auth) {
|
|
298
|
+
const token = auth.Token ?? '';
|
|
299
|
+
return {
|
|
300
|
+
'Authorization': `Bearer ${token}`,
|
|
301
|
+
'Accept': 'application/json',
|
|
302
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
303
|
+
'OData-MaxVersion': '4.0',
|
|
304
|
+
'OData-Version': '4.0',
|
|
305
|
+
// Ask Dataverse to return the created/updated record so create can read OData-EntityId / body.
|
|
306
|
+
'Prefer': 'return=representation',
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* HTTP transport with retry/backoff on 429/503/504 (Dataverse service-protection limits). The
|
|
311
|
+
* thrown error on a retryable status carries the response headers so {@link ExtractRetryAfterMs}
|
|
312
|
+
* can read `Retry-After` for the engine's adaptive bucket.
|
|
313
|
+
*/
|
|
314
|
+
async MakeHTTPRequest(auth, url, method, headers, body) {
|
|
315
|
+
const config = auth.Config;
|
|
316
|
+
const maxRetries = config.MaxRetries ?? DEFAULT_MAX_RETRIES;
|
|
317
|
+
const timeoutMs = config.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
318
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
319
|
+
const response = await this.ExecuteOneRequest(url, method, headers, body, timeoutMs);
|
|
320
|
+
if (this.IsRetryable(response) && attempt < maxRetries) {
|
|
321
|
+
const delay = this.ComputeBackoffDelay(attempt, response.Headers['retry-after']);
|
|
322
|
+
await this.Sleep(delay);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
return response;
|
|
326
|
+
}
|
|
327
|
+
throw new Error(`DynamicsDataverseConnector: exhausted ${maxRetries + 1} attempts for ${method} ${url}`);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Unwraps the OData collection envelope's `value` array. A single-record GET (`/<set>(id)`) is
|
|
331
|
+
* returned as a one-element array. Honors a metadata-declared `ResponseDataKey` if set, defaulting
|
|
332
|
+
* to the OData `value` convention.
|
|
333
|
+
*/
|
|
334
|
+
NormalizeResponse(rawBody, responseDataKey) {
|
|
335
|
+
if (rawBody == null || typeof rawBody !== 'object')
|
|
336
|
+
return [];
|
|
337
|
+
const body = rawBody;
|
|
338
|
+
const key = responseDataKey ?? 'value';
|
|
339
|
+
const data = body[key];
|
|
340
|
+
if (Array.isArray(data))
|
|
341
|
+
return data;
|
|
342
|
+
// Single-record detail response (has at least one non-annotation key) → wrap.
|
|
343
|
+
const hasRecordShape = Object.keys(body).some(k => !this.IsODataAnnotationKey(k));
|
|
344
|
+
return hasRecordShape ? [body] : [];
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Cursor pagination via `@odata.nextLink`. The nextLink is an ABSOLUTE continuation URL that
|
|
348
|
+
* encodes the exact next request (including the opaque `$skiptoken`); it is followed VERBATIM and
|
|
349
|
+
* never hand-built. A present nextLink means another page exists.
|
|
350
|
+
*/
|
|
351
|
+
ExtractPaginationInfo(rawBody, _paginationType, _currentPage, _currentOffset, _pageSize) {
|
|
352
|
+
const body = rawBody;
|
|
353
|
+
const nextLink = body?.['@odata.nextLink'];
|
|
354
|
+
return {
|
|
355
|
+
HasMore: typeof nextLink === 'string' && nextLink.length > 0,
|
|
356
|
+
NextCursor: typeof nextLink === 'string' ? nextLink : undefined,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Per-connection base the engine joins the IO `APIPath` onto. The frozen Dataverse IO APIPaths are
|
|
361
|
+
* ABSOLUTE-from-root (they already include `/api/data/v9.2/...`), so this returns the org ROOT
|
|
362
|
+
* (`<EnvironmentUrl>`) and the resolved request URL is `<EnvironmentUrl>/api/data/v9.2/<entityset>`.
|
|
363
|
+
* The versioned Web API base (`<EnvironmentUrl>/api/data/v9.2`) is `ApiBaseUrl`, used by the
|
|
364
|
+
* connector's OWN calls (WhoAmI / EntityDefinitions / delta). Never a hardcoded org URL.
|
|
365
|
+
*/
|
|
366
|
+
GetBaseURL(_companyIntegration, auth) {
|
|
367
|
+
return auth.BaseUrl;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Honors the OData `@odata.nextLink` cursor (used verbatim) and otherwise injects the page-size
|
|
371
|
+
* request as a `$top`-free first page — Dataverse caps page size via the `Prefer: odata.maxpagesize`
|
|
372
|
+
* header (added in {@link MakeFirstPageHeaders}), NOT a query param, and does NOT support `$skip`.
|
|
373
|
+
*/
|
|
374
|
+
BuildPaginatedURL(basePath, _obj, _page, _offset, cursor, _effectivePageSize) {
|
|
375
|
+
// A cursor is the full nextLink continuation URL — issue it exactly as Dataverse returned it.
|
|
376
|
+
if (cursor && cursor.length > 0)
|
|
377
|
+
return cursor;
|
|
378
|
+
return basePath;
|
|
379
|
+
}
|
|
380
|
+
// ─── Create-response ID extraction (OData-EntityId header) ───────
|
|
381
|
+
/**
|
|
382
|
+
* Dataverse returns the created record's URI in the `OData-EntityId` response header
|
|
383
|
+
* (e.g. `https://org.crm.dynamics.com/api/data/v9.2/accounts(00000000-...)`); the GUID is the
|
|
384
|
+
* trailing `(...)` segment. Falls back to the `Location` header, then the body PK, so a tenant
|
|
385
|
+
* configured to return-representation still resolves. An empty ID makes the create FAIL LOUDLY
|
|
386
|
+
* via the base's BuildCreatedResult (never a silent duplicate-create).
|
|
387
|
+
*/
|
|
388
|
+
ExtractIDFromResponse(response, idLocation) {
|
|
389
|
+
if (!idLocation || idLocation === 'header') {
|
|
390
|
+
const headers = response.Headers ?? {};
|
|
391
|
+
const entityId = headers['odata-entityid'] ?? headers['OData-EntityId'] ?? headers['location'] ?? headers['Location'];
|
|
392
|
+
if (typeof entityId === 'string' && entityId.length > 0) {
|
|
393
|
+
const m = entityId.match(/\(([^)]+)\)\s*$/); // GUID inside the trailing (...)
|
|
394
|
+
if (m)
|
|
395
|
+
return m[1];
|
|
396
|
+
const seg = entityId.match(/[/=]([^/?&#]+)$/);
|
|
397
|
+
return seg ? seg[1] : entityId;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// Body fallback — return=representation echoes the record; the PK is `<set-singular>id` but the
|
|
401
|
+
// generic id-field scan also covers `id`.
|
|
402
|
+
if (response.Body && typeof response.Body === 'object') {
|
|
403
|
+
const b = response.Body;
|
|
404
|
+
for (const k of Object.keys(b)) {
|
|
405
|
+
if (/id$/i.test(k) && (typeof b[k] === 'string' || typeof b[k] === 'number')) {
|
|
406
|
+
const v = String(b[k]);
|
|
407
|
+
if (v.length > 0)
|
|
408
|
+
return v;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return super.ExtractIDFromResponse(response, idLocation);
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Substitutes the target ID into a Dataverse single-record path. Supports the standard GUID form
|
|
416
|
+
* `/<entityset>(id)` AND the alternate-key upsert form `/<entityset>(<altkey>='<value>')` — when
|
|
417
|
+
* the ExternalID already contains an `=` it is an alternate-key expression and is inserted as-is
|
|
418
|
+
* (only the value is escaped); otherwise it is treated as a GUID. Handles both `({id})` template
|
|
419
|
+
* placeholders and a bare `(id)` suffix.
|
|
420
|
+
*/
|
|
421
|
+
SubstituteIDInPath(path, externalID, idLocation) {
|
|
422
|
+
if (idLocation && idLocation !== 'path')
|
|
423
|
+
return path;
|
|
424
|
+
const keyExpr = this.BuildKeyExpression(externalID);
|
|
425
|
+
// Replace an explicit {id}/{ID}/{ExternalID} placeholder if present...
|
|
426
|
+
if (/\{(?:id|ID|ExternalID)\}/.test(path)) {
|
|
427
|
+
return path
|
|
428
|
+
.replace(/\{ID\}/g, keyExpr)
|
|
429
|
+
.replace(/\{id\}/g, keyExpr)
|
|
430
|
+
.replace(/\{ExternalID\}/g, keyExpr);
|
|
431
|
+
}
|
|
432
|
+
// ...otherwise inject into the `(...)` segment the metadata path declares (`/accounts({id})`
|
|
433
|
+
// came pre-substituted above; a literal `/accounts()` or `/accounts` gets the key appended).
|
|
434
|
+
if (/\(\s*\)\s*$/.test(path)) {
|
|
435
|
+
return path.replace(/\(\s*\)\s*$/, `(${keyExpr})`);
|
|
436
|
+
}
|
|
437
|
+
return `${path.replace(/\/+$/, '')}(${keyExpr})`;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Builds the OData key expression for a single-record path. A plain GUID → `00000000-...`. An
|
|
441
|
+
* alternate-key expression (`key='value'` or `k1='v1',k2='v2'`) is passed through with each value
|
|
442
|
+
* single-quote-escaped. This realizes the alternate-key upsert path form the metadata may declare.
|
|
443
|
+
*/
|
|
444
|
+
BuildKeyExpression(externalID) {
|
|
445
|
+
if (externalID.includes('=')) {
|
|
446
|
+
// Alternate-key form: keep `key='...'` pairs, escape embedded single quotes in values.
|
|
447
|
+
return externalID.replace(/'([^']*)'/g, (_full, v) => `'${v.replace(/'/g, "''")}'`);
|
|
448
|
+
}
|
|
449
|
+
return externalID; // GUID — Dataverse accepts the bare GUID inside the parentheses.
|
|
450
|
+
}
|
|
451
|
+
// ─── Change-tracking delta sync ──────────────────────────────────
|
|
452
|
+
/** Whether a table is configured for change tracking (Configuration.changeTrackingHeader present). */
|
|
453
|
+
ObjectUsesChangeTracking(obj) {
|
|
454
|
+
const header = this.readObjectConfigString(obj, 'changeTrackingHeader');
|
|
455
|
+
return header != null && /track-changes/i.test(header);
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Fetches changed records via Dataverse change tracking. The watermark holds the previous poll's
|
|
459
|
+
* `@odata.deltaLink` (a full URL carrying `$deltatoken`) — followed VERBATIM. Deleted records
|
|
460
|
+
* arrive as `$deletedEntity` references; they are surfaced with `IsDeleted=true`. The last page's
|
|
461
|
+
* `@odata.deltaLink` becomes the new watermark for the next poll.
|
|
462
|
+
*/
|
|
463
|
+
async FetchChangesViaDelta(ctx, obj) {
|
|
464
|
+
const auth = await this.Authenticate(ctx.CompanyIntegration, ctx.ContextUser);
|
|
465
|
+
const fields = this.GetCachedFields(obj.ID);
|
|
466
|
+
const pkFieldNames = fields.filter(f => f.IsPrimaryKey).map(f => f.Name);
|
|
467
|
+
// The watermark is the prior deltaLink (a full URL); if it's not a URL, build the initial
|
|
468
|
+
// delta request off the object's read path with the track-changes Prefer header.
|
|
469
|
+
const deltaUrl = ctx.WatermarkValue && /^https?:\/\//.test(ctx.WatermarkValue)
|
|
470
|
+
? ctx.WatermarkValue
|
|
471
|
+
: this.buildDataverseURL(auth.BaseUrl, obj.APIPath);
|
|
472
|
+
const headers = { ...this.BuildHeaders(auth), Prefer: 'odata.track-changes,return=representation' };
|
|
473
|
+
const response = await this.MakeHTTPRequest(auth, deltaUrl, 'GET', headers);
|
|
474
|
+
if (response.Status < 200 || response.Status >= 300) {
|
|
475
|
+
throw new Error(`Dataverse delta fetch failed for "${obj.Name}": HTTP ${response.Status} — ${this.previewBody(response.Body)}`);
|
|
476
|
+
}
|
|
477
|
+
const body = response.Body;
|
|
478
|
+
const rows = body.value ?? [];
|
|
479
|
+
const nextLink = body['@odata.nextLink'];
|
|
480
|
+
const deltaLink = body['@odata.deltaLink'];
|
|
481
|
+
const records = rows.map(r => this.DeltaRowToExternalRecord(r, obj, fields, pkFieldNames));
|
|
482
|
+
return {
|
|
483
|
+
Records: records,
|
|
484
|
+
HasMore: typeof nextLink === 'string' && nextLink.length > 0,
|
|
485
|
+
NextCursor: typeof nextLink === 'string' ? nextLink : undefined,
|
|
486
|
+
// Only persist the new high-water deltaLink once the full delta set has drained (last page).
|
|
487
|
+
NewWatermarkValue: typeof deltaLink === 'string' ? deltaLink : undefined,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
/** Maps one change-tracking row (live record OR `$deletedEntity` tombstone) to an ExternalRecord. */
|
|
491
|
+
DeltaRowToExternalRecord(raw, obj, fields, pkFieldNames) {
|
|
492
|
+
const isDeleted = typeof raw['@odata.context'] === 'string' && /\$deletedEntity/i.test(raw['@odata.context']);
|
|
493
|
+
const transformed = this.applyTransformPreservingKeys(raw, obj, fields);
|
|
494
|
+
const idFieldName = pkFieldNames[0];
|
|
495
|
+
const externalID = idFieldName && transformed[idFieldName] != null
|
|
496
|
+
? String(transformed[idFieldName])
|
|
497
|
+
: (typeof raw['id'] === 'string' ? raw['id'] : '');
|
|
498
|
+
return {
|
|
499
|
+
ExternalID: externalID,
|
|
500
|
+
ObjectType: obj.Name,
|
|
501
|
+
Fields: transformed, // full source record (minus annotations) — custom-column pass-through
|
|
502
|
+
IsDeleted: isDeleted,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
// ─── EntityDefinitions describe helpers ──────────────────────────
|
|
506
|
+
/** Fetches the full table list via `EntityDefinitions` (paged, following @odata.nextLink). */
|
|
507
|
+
async FetchEntityDefinitions(companyIntegration, contextUser) {
|
|
508
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
509
|
+
const select = '$select=LogicalName,EntitySetName,DisplayName,Description,PrimaryIdAttribute,PrimaryNameAttribute,ChangeTrackingEnabled,IsCustomEntity';
|
|
510
|
+
let url = `${auth.ApiBaseUrl}/EntityDefinitions?${select}`;
|
|
511
|
+
const out = [];
|
|
512
|
+
while (url) {
|
|
513
|
+
const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
|
|
514
|
+
if (response.Status < 200 || response.Status >= 300) {
|
|
515
|
+
throw new Error(`EntityDefinitions describe failed: HTTP ${response.Status} — ${this.previewBody(response.Body)}`);
|
|
516
|
+
}
|
|
517
|
+
const body = response.Body;
|
|
518
|
+
for (const e of body.value ?? [])
|
|
519
|
+
out.push(e);
|
|
520
|
+
url = typeof body['@odata.nextLink'] === 'string' ? body['@odata.nextLink'] : undefined;
|
|
521
|
+
}
|
|
522
|
+
return out;
|
|
523
|
+
}
|
|
524
|
+
/** Describes one table + its Attributes by logical name. Returns null if not visible to the credential. */
|
|
525
|
+
async FetchEntityDefinition(companyIntegration, objectName, contextUser) {
|
|
526
|
+
const auth = await this.Authenticate(companyIntegration, contextUser);
|
|
527
|
+
const logicalName = this.ResolveLogicalName(companyIntegration, objectName);
|
|
528
|
+
const expand = '$expand=Attributes($select=LogicalName,SchemaName,DisplayName,Description,AttributeType,MaxLength,Precision,IsValidForCreate,IsValidForUpdate,IsPrimaryId,RequiredLevel,Targets)';
|
|
529
|
+
const select = '$select=LogicalName,EntitySetName,DisplayName,Description,PrimaryIdAttribute,PrimaryNameAttribute,ChangeTrackingEnabled,IsCustomEntity';
|
|
530
|
+
const url = `${auth.ApiBaseUrl}/EntityDefinitions(LogicalName='${encodeURIComponent(logicalName)}')?${select}&${expand}`;
|
|
531
|
+
const response = await this.MakeHTTPRequest(auth, url, 'GET', this.BuildHeaders(auth));
|
|
532
|
+
if (response.Status === 404)
|
|
533
|
+
return null;
|
|
534
|
+
if (response.Status < 200 || response.Status >= 300) {
|
|
535
|
+
throw new Error(`EntityDefinition describe failed for "${objectName}": HTTP ${response.Status} — ${this.previewBody(response.Body)}`);
|
|
536
|
+
}
|
|
537
|
+
return response.Body;
|
|
538
|
+
}
|
|
539
|
+
/** Resolves the Dataverse logical name for an IO (Configuration.logicalName, else the IO name). */
|
|
540
|
+
ResolveLogicalName(companyIntegration, objectName) {
|
|
541
|
+
const obj = this.tryGetCachedObjectFor(companyIntegration.IntegrationID, objectName);
|
|
542
|
+
if (obj) {
|
|
543
|
+
const logical = this.readObjectConfigString(obj, 'logicalName');
|
|
544
|
+
if (logical)
|
|
545
|
+
return logical;
|
|
546
|
+
}
|
|
547
|
+
return objectName;
|
|
548
|
+
}
|
|
549
|
+
/** Maps a Dataverse table describe entry → ExternalObjectSchema. */
|
|
550
|
+
EntityMetadataToObjectSchema(e) {
|
|
551
|
+
const name = e.LogicalName;
|
|
552
|
+
return {
|
|
553
|
+
Name: name,
|
|
554
|
+
Label: this.LocalizedLabel(e.DisplayName) ?? name,
|
|
555
|
+
Description: this.LocalizedLabel(e.Description) ?? undefined,
|
|
556
|
+
SupportsIncrementalSync: e.ChangeTrackingEnabled === true || true, // modifiedon fallback always available
|
|
557
|
+
SupportsWrite: true, // Dataverse tables are generally CRUD-capable; per-table messages refine downstream
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
/** Maps a Dataverse attribute describe entry → ExternalFieldSchema. */
|
|
561
|
+
AttributeMetadataToFieldSchema(a, primaryId) {
|
|
562
|
+
const name = a.LogicalName;
|
|
563
|
+
const isPk = a.IsPrimaryId === true || name === primaryId;
|
|
564
|
+
const requiredLevel = a.RequiredLevel?.Value;
|
|
565
|
+
const isRequired = requiredLevel === 'SystemRequired' || requiredLevel === 'ApplicationRequired';
|
|
566
|
+
// A scalar Lookup attribute that names a single target table is a FK. A polymorphic lookup
|
|
567
|
+
// (multiple Targets) or non-Lookup attribute is NOT emitted as an FK (provable-only).
|
|
568
|
+
const isLookup = a.AttributeType === 'Lookup' || a.AttributeType === 'Customer' || a.AttributeType === 'Owner';
|
|
569
|
+
const singleTarget = isLookup && Array.isArray(a.Targets) && a.Targets.length === 1 ? a.Targets[0] : null;
|
|
570
|
+
return {
|
|
571
|
+
Name: name,
|
|
572
|
+
Label: this.LocalizedLabel(a.DisplayName) ?? name,
|
|
573
|
+
Description: this.LocalizedLabel(a.Description) ?? undefined,
|
|
574
|
+
DataType: a.AttributeType ?? 'String',
|
|
575
|
+
IsRequired: isRequired,
|
|
576
|
+
IsUniqueKey: isPk,
|
|
577
|
+
IsPrimaryKey: isPk,
|
|
578
|
+
IsReadOnly: a.IsValidForCreate === false && a.IsValidForUpdate === false,
|
|
579
|
+
IsForeignKey: singleTarget != null,
|
|
580
|
+
ForeignKeyTarget: singleTarget,
|
|
581
|
+
MaxLength: typeof a.MaxLength === 'number' ? a.MaxLength : null,
|
|
582
|
+
Precision: typeof a.Precision === 'number' ? a.Precision : null,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
/** Reads a Dataverse localized label, falling back to undefined. */
|
|
586
|
+
LocalizedLabel(label) {
|
|
587
|
+
const text = label?.UserLocalizedLabel?.Label;
|
|
588
|
+
return typeof text === 'string' && text.length > 0 ? text : undefined;
|
|
589
|
+
}
|
|
590
|
+
// ─── Config parsing ──────────────────────────────────────────────
|
|
591
|
+
/** Parses the connection config, preferring the attached MJ Credential over Configuration JSON. */
|
|
592
|
+
async ParseConfig(companyIntegration, contextUser) {
|
|
593
|
+
if (companyIntegration.CredentialID) {
|
|
594
|
+
return this.ParseConfigFromCredential(companyIntegration.CredentialID, contextUser);
|
|
595
|
+
}
|
|
596
|
+
if (companyIntegration.Configuration) {
|
|
597
|
+
return this.ValidateConfig(JSON.parse(companyIntegration.Configuration));
|
|
598
|
+
}
|
|
599
|
+
throw new Error('DynamicsDataverseConnector requires either CredentialID or Configuration JSON on the CompanyIntegration');
|
|
600
|
+
}
|
|
601
|
+
/** Loads the OAuth2 config from the MJ: Credentials entity Values JSON. */
|
|
602
|
+
async ParseConfigFromCredential(credentialID, contextUser, provider) {
|
|
603
|
+
const md = provider ?? new Metadata();
|
|
604
|
+
const cred = await md.GetEntityObject('MJ: Credentials', contextUser);
|
|
605
|
+
const loaded = await cred.Load(credentialID);
|
|
606
|
+
if (!loaded || !cred.Values) {
|
|
607
|
+
throw new Error('Dynamics credential could not be loaded or has no Values JSON');
|
|
608
|
+
}
|
|
609
|
+
return this.ValidateConfig(JSON.parse(cred.Values));
|
|
610
|
+
}
|
|
611
|
+
/** Validates the parsed config + applies defaults. Field names are read case-insensitively. */
|
|
612
|
+
ValidateConfig(raw) {
|
|
613
|
+
if (!raw || typeof raw !== 'object') {
|
|
614
|
+
throw new Error('Dynamics configuration is not a valid object');
|
|
615
|
+
}
|
|
616
|
+
const obj = raw;
|
|
617
|
+
const getStr = (...keys) => {
|
|
618
|
+
for (const key of keys) {
|
|
619
|
+
const lower = key.toLowerCase();
|
|
620
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
621
|
+
if (k.toLowerCase() === lower && typeof v === 'string' && v.length > 0)
|
|
622
|
+
return v;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return undefined;
|
|
626
|
+
};
|
|
627
|
+
const getNum = (...keys) => {
|
|
628
|
+
for (const key of keys) {
|
|
629
|
+
const lower = key.toLowerCase();
|
|
630
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
631
|
+
if (k.toLowerCase() === lower && typeof v === 'number')
|
|
632
|
+
return v;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return undefined;
|
|
636
|
+
};
|
|
637
|
+
const tenantId = getStr('tenantid', 'tenant_id', 'tenant');
|
|
638
|
+
const clientId = getStr('clientid', 'client_id', 'applicationid', 'appid');
|
|
639
|
+
const clientSecret = getStr('clientsecret', 'client_secret', 'secret');
|
|
640
|
+
const environmentUrl = getStr('environmenturl', 'environment_url', 'resource', 'orgurl', 'org_url', 'baseurl', 'base_url');
|
|
641
|
+
if (!tenantId)
|
|
642
|
+
throw new Error('Dynamics configuration missing required field: TenantId');
|
|
643
|
+
if (!clientId)
|
|
644
|
+
throw new Error('Dynamics configuration missing required field: ClientId');
|
|
645
|
+
if (!clientSecret)
|
|
646
|
+
throw new Error('Dynamics configuration missing required field: ClientSecret');
|
|
647
|
+
if (!environmentUrl)
|
|
648
|
+
throw new Error('Dynamics configuration missing required field: EnvironmentUrl');
|
|
649
|
+
return {
|
|
650
|
+
TenantId: tenantId,
|
|
651
|
+
ClientId: clientId,
|
|
652
|
+
ClientSecret: clientSecret,
|
|
653
|
+
EnvironmentUrl: environmentUrl.replace(/\/+$/, ''),
|
|
654
|
+
ApiVersion: getStr('apiversion', 'api_version') ?? DEFAULT_API_VERSION,
|
|
655
|
+
AuthorityHost: getStr('authorityhost', 'authority_host', 'authority'),
|
|
656
|
+
Scope: getStr('scope', 'scopes'),
|
|
657
|
+
MaxRetries: getNum('maxretries') ?? DEFAULT_MAX_RETRIES,
|
|
658
|
+
RequestTimeoutMs: getNum('requesttimeoutms') ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
659
|
+
MinRequestIntervalMs: getNum('minrequestintervalms') ?? 0,
|
|
660
|
+
MaxPageSize: Math.min(getNum('maxpagesize') ?? MAX_ODATA_PAGE_SIZE, MAX_ODATA_PAGE_SIZE),
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
/** Resolves the Web API base URL: `<EnvironmentUrl>/api/data/v9.2`. */
|
|
664
|
+
ResolveBaseUrl(config) {
|
|
665
|
+
const version = config.ApiVersion ?? DEFAULT_API_VERSION;
|
|
666
|
+
return `${config.EnvironmentUrl.replace(/\/+$/, '')}/api/data/${version}`;
|
|
667
|
+
}
|
|
668
|
+
// ─── HTTP helpers ────────────────────────────────────────────────
|
|
669
|
+
async ExecuteOneRequest(url, method, headers, body, timeoutMs) {
|
|
670
|
+
const controller = new AbortController();
|
|
671
|
+
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
|
|
672
|
+
try {
|
|
673
|
+
const response = await fetch(url, {
|
|
674
|
+
method,
|
|
675
|
+
headers,
|
|
676
|
+
body: body !== undefined && method !== 'GET' && method !== 'DELETE'
|
|
677
|
+
? JSON.stringify(body)
|
|
678
|
+
: undefined,
|
|
679
|
+
signal: controller.signal,
|
|
680
|
+
});
|
|
681
|
+
const responseHeaders = this.ExtractHeaders(response.headers);
|
|
682
|
+
const parsedBody = await this.ParseResponseBody(response);
|
|
683
|
+
return { Status: response.status, Body: parsedBody, Headers: responseHeaders };
|
|
684
|
+
}
|
|
685
|
+
finally {
|
|
686
|
+
clearTimeout(timeoutHandle);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
async ParseResponseBody(response) {
|
|
690
|
+
const text = await response.text();
|
|
691
|
+
if (!text)
|
|
692
|
+
return null;
|
|
693
|
+
try {
|
|
694
|
+
return JSON.parse(text);
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
return text;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
ExtractHeaders(headers) {
|
|
701
|
+
const map = {};
|
|
702
|
+
headers.forEach((value, key) => {
|
|
703
|
+
map[key.toLowerCase()] = value;
|
|
704
|
+
});
|
|
705
|
+
return map;
|
|
706
|
+
}
|
|
707
|
+
IsRetryable(response) {
|
|
708
|
+
return response.Status === 429 || response.Status === 503 || response.Status === 504;
|
|
709
|
+
}
|
|
710
|
+
ComputeBackoffDelay(attempt, retryAfterHeader) {
|
|
711
|
+
if (retryAfterHeader) {
|
|
712
|
+
const parsed = parseInt(retryAfterHeader, 10);
|
|
713
|
+
if (!Number.isNaN(parsed) && parsed > 0) {
|
|
714
|
+
return Math.min(parsed * 1000, 120_000);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return Math.min(Math.pow(2, attempt) * 1000, 30_000);
|
|
718
|
+
}
|
|
719
|
+
Sleep(ms) {
|
|
720
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
721
|
+
}
|
|
722
|
+
// ─── Small utilities ─────────────────────────────────────────────
|
|
723
|
+
/**
|
|
724
|
+
* True for any OData/Dataverse annotation key. Genuine columns never contain `@`; every annotation
|
|
725
|
+
* does — the response-level controls (`@odata.etag`, `@odata.context`, …) and the per-column
|
|
726
|
+
* suffixes (`<col>@odata.bind`, `<col>@OData.Community.Display.V1.FormattedValue`,
|
|
727
|
+
* `<col>@Microsoft.Dynamics.CRM.lookuplogicalname`). So an `@`-containing key is an annotation.
|
|
728
|
+
*/
|
|
729
|
+
IsODataAnnotationKey(key) {
|
|
730
|
+
return key.includes('@');
|
|
731
|
+
}
|
|
732
|
+
buildDataverseURL(baseURL, apiPath) {
|
|
733
|
+
const base = baseURL.replace(/\/+$/, '');
|
|
734
|
+
const path = apiPath.startsWith('/') ? apiPath : `/${apiPath}`;
|
|
735
|
+
return `${base}${path}`;
|
|
736
|
+
}
|
|
737
|
+
previewBody(body) {
|
|
738
|
+
const s = typeof body === 'string' ? body : JSON.stringify(body);
|
|
739
|
+
return (s ?? '').slice(0, 500);
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Best-effort cached-object lookup by NAME using the last-seen integration ID. Used by
|
|
743
|
+
* {@link StableOrderingKey}, which the engine calls without a CompanyIntegration; returns null
|
|
744
|
+
* (keyset resume simply unavailable) before any fetch has run.
|
|
745
|
+
*/
|
|
746
|
+
tryGetCachedObject(objectName) {
|
|
747
|
+
if (!this.lastIntegrationID)
|
|
748
|
+
return null;
|
|
749
|
+
return this.tryGetCachedObjectFor(this.lastIntegrationID, objectName);
|
|
750
|
+
}
|
|
751
|
+
tryGetCachedObjectFor(integrationID, objectName) {
|
|
752
|
+
if (!integrationID)
|
|
753
|
+
return null;
|
|
754
|
+
try {
|
|
755
|
+
return this.GetCachedObject(integrationID, objectName);
|
|
756
|
+
}
|
|
757
|
+
catch {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
/** Reads a trimmed string from an IO's Configuration JSON, tolerant of absent/invalid. */
|
|
762
|
+
readObjectConfigString(obj, key) {
|
|
763
|
+
const raw = obj.Configuration;
|
|
764
|
+
if (!raw || typeof raw !== 'string')
|
|
765
|
+
return null;
|
|
766
|
+
try {
|
|
767
|
+
const cfg = JSON.parse(raw);
|
|
768
|
+
const v = cfg[key];
|
|
769
|
+
return typeof v === 'string' && v.trim().length > 0 ? v.trim() : null;
|
|
770
|
+
}
|
|
771
|
+
catch {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/** Extracts response headers from a thrown error, when the connector throws an error carrying them. */
|
|
776
|
+
extractHeadersFromError(error) {
|
|
777
|
+
if (error && typeof error === 'object') {
|
|
778
|
+
const e = error;
|
|
779
|
+
return e.Headers ?? e.headers;
|
|
780
|
+
}
|
|
781
|
+
return undefined;
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
DynamicsDataverseConnector = __decorate([
|
|
785
|
+
RegisterClass(BaseIntegrationConnector, '@memberjunction/connector-microsoft-dynamics-365-dataverse')
|
|
786
|
+
], DynamicsDataverseConnector);
|
|
787
|
+
export { DynamicsDataverseConnector };
|
|
788
|
+
/** Tree-shaking prevention function — import and call from the package entry point. */
|
|
789
|
+
export function LoadDynamicsDataverseConnector() { }
|
|
790
|
+
//# sourceMappingURL=DynamicsDataverseConnector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DynamicsDataverseConnector.js","sourceRoot":"","sources":["../src/DynamicsDataverseConnector.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAyC,MAAM,sBAAsB,CAAC;AAOvF,OAAO,EACH,wBAAwB,EACxB,4BAA4B,EAC5B,kBAAkB,GAarB,MAAM,oCAAoC,CAAC;AAqG5C,wEAAwE;AAExE,uCAAuC;AACvC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAEnC,uCAAuC;AACvC,MAAM,sBAAsB,GAAG,mCAAmC,CAAC;AAEnE,uEAAuE;AACvE,MAAM,0BAA0B,GAAG,MAAM,CAAC;AAE1C,qEAAqE;AACrE,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAE9B,6FAA6F;AAC7F,MAAM,mBAAmB,GAAG,KAAK,CAAC;AAElC;;;;;;;;;;;;;;;;;;;GAmBG;AAEI,IAAM,0BAA0B,GAAhC,MAAM,0BAA2B,SAAQ,4BAA4B;IAArE;QAEH,oEAAoE;;QAEnD,iBAAY,GAAG,IAAI,kBAAkB,EAAE,CAAC;QACjD,eAAU,GAA+B,IAAI,CAAC;QA8yBtD,sGAAsG;QAC9F,sBAAiB,GAAkB,IAAI,CAAC;IA0CpD,CAAC;IAv1BG,oEAAoE;IAEpE;;iEAE6D;IAC7D,IAAoB,eAAe,KAAa,OAAO,oCAAoC,CAAC,CAAC,CAAC;IAE9F,IAAoB,cAAc,KAAc,OAAO,IAAI,CAAC,CAAC,CAAC;IAC9D,IAAoB,cAAc,KAAc,OAAO,IAAI,CAAC,CAAC,CAAC;IAC9D,IAAoB,cAAc,KAAc,OAAO,IAAI,CAAC,CAAC,CAAC;IAE9D;;;;OAIG;IACH,IAAoB,wBAAwB,KAAc,OAAO,IAAI,CAAC,CAAC,CAAC;IAExE;;;;;OAKG;IACH,IAAoB,kBAAkB,KAAc,OAAO,IAAI,CAAC,CAAC,CAAC;IAElE;;;;OAIG;IACa,iBAAiB,CAAC,UAAkB;QAChD,MAAM,GAAG,GAAG,IAAI,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;QAChD,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC;QACtB,OAAO,IAAI,CAAC,sBAAsB,CAAC,GAAG,EAAE,oBAAoB,CAAC;eACtD,IAAI,CAAC,sBAAsB,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAC;IACjE,CAAC;IAED;;;;;OAKG;IACH,IAAoB,eAAe;QAC/B,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,qBAAqB,EAAE,GAAG,EAAE,CAAC;IACvE,CAAC;IAED,IAAoB,kBAAkB,KAAoB,OAAO,CAAC,CAAC,CAAC,CAAC;IAErE;;;OAGG;IACa,mBAAmB,CAAC,KAAc;QAC9C,MAAM,OAAO,GAAG,IAAI,CAAC,uBAAuB,CAAC,KAAK,CAAC,CAAC;QACpD,IAAI,CAAC,OAAO;YAAE,OAAO,SAAS,CAAC;QAC/B,MAAM,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;QAC1C,IAAI,UAAU,EAAE,CAAC;YACb,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;YACrC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC;gBAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,GAAG,KAAK,CAAC,CAAC;YACpE,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE,CAAC;YAC9C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;gBAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QACvE,CAAC;QACD,OAAO,SAAS,CAAC;IACrB,CAAC;IAED,oEAAoE;IAEpE;;;OAGG;IACI,KAAK,CAAC,cAAc,CACvB,kBAA8C,EAC9C,WAAqB;QAErB,IAAI,CAAC;YACD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,kBAAkB,EAAE,WAAW,EAAE,IAAI,CAAC,CAAC;YAC5E,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,UAAU,SAAS,CAAC;YACxC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;YACvF,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;gBAClD,OAAO;oBACH,OAAO,EAAE,KAAK;oBACd,OAAO,EAAE,iCAAiC,QAAQ,CAAC,MAAM,MAAM,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE;iBACnG,CAAC;YACN,CAAC;YACD,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAoD,CAAC;YAC3E,OAAO;gBACH,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE,+BAA+B,IAAI,CAAC,cAAc,IAAI,SAAS,GAAG;gBAC3E,aAAa,EAAE,WAAW,IAAI,CAAC,MAAM,CAAC,UAAU,IAAI,mBAAmB,EAAE;aAC5E,CAAC;QACN,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACjE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,sBAAsB,OAAO,EAAE,EAAE,CAAC;QACxE,CAAC;IACL,CAAC;IAED,uEAAuE;IAEvE;;;;;;OAMG;IACa,KAAK,CAAC,eAAe,CACjC,kBAA8C,EAC9C,WAAqB;QAErB,IAAI,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,sBAAsB,CAAC,kBAAkB,EAAE,WAAW,CAAC,CAAC;YACpF,OAAO,QAAQ;iBACV,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,WAAW,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,aAAa,KAAK,QAAQ,IAAI,CAAC,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC,CAAC;iBACnH,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC,CAAC,CAAC,CAAC;QACxD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACX,2FAA2F;YAC3F,4FAA4F;YAC5F,yFAAyF;YACzF,0FAA0F;YAC1F,kFAAkF;YAClF,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC7D,IAAI,mGAAmG,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBAChH,MAAM,IAAI,KAAK,CAAC,wGAAwG,GAAG,EAAE,CAAC,CAAC;YACnI,CAAC;YACD,MAAM,GAAG,CAAC;QACd,CAAC;IACL,CAAC;IAED;;;;;OAKG;IACa,KAAK,CAAC,cAAc,CAChC,kBAA8C,EAC9C,UAAkB,EAClB,WAAqB;QAErB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,qBAAqB,CAAC,kBAAkB,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC;QAC7F,IAAI,CAAC,MAAM,EAAE,CAAC;YACV,iFAAiF;YACjF,sFAAsF;YACtF,OAAO,KAAK,CAAC,cAAc,CAAC,kBAAkB,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC;QAC7E,CAAC;QACD,MAAM,SAAS,GAAG,MAAM,CAAC,kBAAkB,IAAI,GAAG,MAAM,CAAC,WAAW,IAAI,CAAC;QACzE,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC;QAC3C,OAAO,UAAU;aACZ,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,WAAW,KAAK,QAAQ,IAAI,CAAC,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC;aAC1E,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC;IACrE,CAAC;IAED,oEAAoE;IAEpE;;;;;OAKG;IACa,KAAK,CAAC,YAAY,CAAC,GAAiB;QAChD,IAAI,CAAC,iBAAiB,GAAG,GAAG,CAAC,kBAAkB,CAAC,aAAa,CAAC;QAC9D,MAAM,GAAG,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,kBAAkB,CAAC,aAAa,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;QACvF,IAAI,GAAG,CAAC,uBAAuB,IAAI,IAAI,CAAC,wBAAwB,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,cAAc,EAAE,CAAC;YAC1F,OAAO,IAAI,CAAC,oBAAoB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAC/C,CAAC;QACD,OAAO,KAAK,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;IAED,sEAAsE;IAEtE;;;;;;OAMG;IACgB,eAAe,CAC9B,GAA4B,EAC5B,IAA+B,EAC/B,OAAyC;QAEzC,MAAM,GAAG,GAA4B,EAAE,CAAC;QACxC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAC7C,IAAI,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC;gBAAE,SAAS;YAC7C,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACrB,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;;;;;;;OAQG;IACgB,kBAAkB,CAAC,WAAmB;QACrD,OAAO,CAAC,aAAa,EAAE,gBAAgB,EAAE,WAAW,EAAE,iBAAiB,EAAE,aAAa,CAAC,CAAC;IAC5F,CAAC;IAED;;;;;;;OAOG;IACgB,4BAA4B,CAC3C,GAA4B,EAC5B,GAA8B,EAC9B,MAAwC;QAExC,MAAM,WAAW,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QAC3D,IAAI,WAAW,KAAK,GAAG;YAAE,OAAO,GAAG,CAAC;QACpC,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QAC5D,MAAM,GAAG,GAA4B,EAAE,GAAG,WAAW,EAAE,CAAC;QACxD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACjC,IAAI,GAAG,IAAI,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC;gBAAE,SAAS;YAChF,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;QACxB,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED,mEAAmE;IAEnE;;;;OAIG;IACO,KAAK,CAAC,YAAY,CACxB,kBAA8C,EAC9C,WAAqB,EACrB,YAAY,GAAG,KAAK;QAEpB,IAAI,YAAY,EAAE,CAAC;YACf,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;YACvB,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;QAC9B,CAAC;aAAM,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACzB,iFAAiF;YACjF,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;YAC/D,OAAO,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;QACpD,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,kBAAkB,EAAE,WAAW,CAAC,CAAC;QACvE,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC3C,MAAM,IAAI,GAAwB;YAC9B,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,gDAAgD;YACpG,UAAU,EAAE,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,EAAa,+CAA+C;SACtG,CAAC;QACF,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACvB,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,uFAAuF;IAC/E,KAAK,CAAC,SAAS,CAAC,MAAgC;QACpD,MAAM,aAAa,GAAG,CAAC,MAAM,CAAC,aAAa,IAAI,sBAAsB,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC3F,MAAM,QAAQ,GAAG,GAAG,aAAa,IAAI,kBAAkB,CAAC,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAAC;QAC7F,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,GAAG,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,WAAW,CAAC;QACtF,MAAM,GAAG,GAAuB;YAC5B,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,YAAY,EAAE,MAAM,CAAC,YAAY;YACjC,2FAA2F;YAC3F,MAAM,EAAE,KAAK;YACb,UAAU,EAAE,OAAO;YACnB,SAAS,EAAE,MAAM,CAAC,gBAAgB,IAAI,0BAA0B;SACnE,CAAC;QACF,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,cAAc,CAAC,GAAG,EAAE,oBAAoB,CAAC,CAAC;QAChF,OAAO,KAAK,CAAC,WAAW,CAAC;IAC7B,CAAC;IAED,sGAAsG;IAC5F,YAAY,CAAC,IAAqB;QACxC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QAC/B,OAAO;YACH,eAAe,EAAE,UAAU,KAAK,EAAE;YAClC,QAAQ,EAAE,kBAAkB;YAC5B,cAAc,EAAE,iCAAiC;YACjD,kBAAkB,EAAE,KAAK;YACzB,eAAe,EAAE,KAAK;YACtB,+FAA+F;YAC/F,QAAQ,EAAE,uBAAuB;SACpC,CAAC;IACN,CAAC;IAED;;;;OAIG;IACO,KAAK,CAAC,eAAe,CAC3B,IAAqB,EACrB,GAAW,EACX,MAAc,EACd,OAA+B,EAC/B,IAAc;QAEd,MAAM,MAAM,GAAI,IAA4B,CAAC,MAAM,CAAC;QACpD,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,IAAI,mBAAmB,CAAC;QAC5D,MAAM,SAAS,GAAG,MAAM,CAAC,gBAAgB,IAAI,0BAA0B,CAAC;QAExE,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;YACrD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;YACrF,IAAI,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,OAAO,GAAG,UAAU,EAAE,CAAC;gBACrD,MAAM,KAAK,GAAG,IAAI,CAAC,mBAAmB,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;gBACjF,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;gBACxB,SAAS;YACb,CAAC;YACD,OAAO,QAAQ,CAAC;QACpB,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,yCAAyC,UAAU,GAAG,CAAC,iBAAiB,MAAM,IAAI,GAAG,EAAE,CAAC,CAAC;IAC7G,CAAC;IAED;;;;OAIG;IACO,iBAAiB,CACvB,OAAgB,EAChB,eAA8B;QAE9B,IAAI,OAAO,IAAI,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,OAAO,EAAE,CAAC;QAC9D,MAAM,IAAI,GAAG,OAAkC,CAAC;QAChD,MAAM,GAAG,GAAG,eAAe,IAAI,OAAO,CAAC;QACvC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;QACvB,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;YAAE,OAAO,IAAiC,CAAC;QAClE,8EAA8E;QAC9E,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC;QAClF,OAAO,cAAc,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACxC,CAAC;IAED;;;;OAIG;IACO,qBAAqB,CAC3B,OAAgB,EAChB,eAA+B,EAC/B,YAAoB,EACpB,cAAsB,EACtB,SAAiB;QAEjB,MAAM,IAAI,GAAG,OAA2C,CAAC;QACzD,MAAM,QAAQ,GAAG,IAAI,EAAE,CAAC,iBAAiB,CAAC,CAAC;QAC3C,OAAO;YACH,OAAO,EAAE,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;YAC5D,UAAU,EAAE,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS;SAClE,CAAC;IACN,CAAC;IAED;;;;;;OAMG;IACO,UAAU,CAChB,mBAA+C,EAC/C,IAAqB;QAErB,OAAQ,IAA4B,CAAC,OAAO,CAAC;IACjD,CAAC;IAED;;;;OAIG;IACgB,iBAAiB,CAChC,QAAgB,EAChB,IAA+B,EAC/B,KAAa,EACb,OAAe,EACf,MAAe,EACf,kBAA2B;QAE3B,8FAA8F;QAC9F,IAAI,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,MAAM,CAAC;QAC/C,OAAO,QAAQ,CAAC;IACpB,CAAC;IAED,oEAAoE;IAEpE;;;;;;OAMG;IACgB,qBAAqB,CAAC,QAAsB,EAAE,UAAyB;QACtF,IAAI,CAAC,UAAU,IAAI,UAAU,KAAK,QAAQ,EAAE,CAAC;YACzC,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,IAAI,EAAE,CAAC;YACvC,MAAM,QAAQ,GAAG,OAAO,CAAC,gBAAgB,CAAC,IAAI,OAAO,CAAC,gBAAgB,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;YACtH,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtD,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,CAAC,iCAAiC;gBAC9E,IAAI,CAAC;oBAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;gBACnB,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;gBAC9C,OAAO,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;YACnC,CAAC;QACL,CAAC;QACD,gGAAgG;QAChG,0CAA0C;QAC1C,IAAI,QAAQ,CAAC,IAAI,IAAI,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACrD,MAAM,CAAC,GAAG,QAAQ,CAAC,IAA+B,CAAC;YACnD,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC7B,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,EAAE,CAAC;oBAC3E,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;oBACvB,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC;wBAAE,OAAO,CAAC,CAAC;gBAC/B,CAAC;YACL,CAAC;QACL,CAAC;QACD,OAAO,KAAK,CAAC,qBAAqB,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAC7D,CAAC;IAED;;;;;;OAMG;IACgB,kBAAkB,CACjC,IAAY,EACZ,UAAkB,EAClB,UAAyB;QAEzB,IAAI,UAAU,IAAI,UAAU,KAAK,MAAM;YAAE,OAAO,IAAI,CAAC;QACrD,MAAM,OAAO,GAAG,IAAI,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;QACpD,uEAAuE;QACvE,IAAI,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACxC,OAAO,IAAI;iBACN,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC;iBAC3B,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC;iBAC3B,OAAO,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;QAC7C,CAAC;QACD,6FAA6F;QAC7F,6FAA6F;QAC7F,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,IAAI,OAAO,GAAG,CAAC,CAAC;QACvD,CAAC;QACD,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,OAAO,GAAG,CAAC;IACrD,CAAC;IAED;;;;OAIG;IACK,kBAAkB,CAAC,UAAkB;QACzC,IAAI,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3B,uFAAuF;YACvF,OAAO,UAAU,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,KAAK,EAAE,CAAS,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QAChG,CAAC;QACD,OAAO,UAAU,CAAC,CAAC,iEAAiE;IACxF,CAAC;IAED,oEAAoE;IAEpE,sGAAsG;IAC9F,wBAAwB,CAAC,GAA8B;QAC3D,MAAM,MAAM,GAAG,IAAI,CAAC,sBAAsB,CAAC,GAAG,EAAE,sBAAsB,CAAC,CAAC;QACxE,OAAO,MAAM,IAAI,IAAI,IAAI,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC3D,CAAC;IAED;;;;;OAKG;IACK,KAAK,CAAC,oBAAoB,CAC9B,GAAiB,EACjB,GAA8B;QAE9B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,kBAAkB,EAAE,GAAG,CAAC,WAAW,CAAC,CAAC;QAC9E,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC5C,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAEzE,0FAA0F;QAC1F,iFAAiF;QACjF,MAAM,QAAQ,GAAG,GAAG,CAAC,cAAc,IAAI,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC;YAC1E,CAAC,CAAC,GAAG,CAAC,cAAc;YACpB,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;QAExD,MAAM,OAAO,GAAG,EAAE,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,2CAA2C,EAAE,CAAC;QACpG,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;QAC5E,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;YAClD,MAAM,IAAI,KAAK,CAAC,qCAAqC,GAAG,CAAC,IAAI,WAAW,QAAQ,CAAC,MAAM,MAAM,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACpI,CAAC;QAED,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAwD,CAAC;QAC/E,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QAC9B,MAAM,QAAQ,GAAG,IAAI,CAAC,iBAAiB,CAAC,CAAC;QACzC,MAAM,SAAS,GAAG,IAAI,CAAC,kBAAkB,CAAC,CAAC;QAE3C,MAAM,OAAO,GAAqB,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;QAE7G,OAAO;YACH,OAAO,EAAE,OAAO;YAChB,OAAO,EAAE,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;YAC5D,UAAU,EAAE,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS;YAC/D,6FAA6F;YAC7F,iBAAiB,EAAE,OAAO,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;SAC3E,CAAC;IACN,CAAC;IAED,qGAAqG;IAC7F,wBAAwB,CAC5B,GAA4B,EAC5B,GAA8B,EAC9B,MAAwC,EACxC,YAAsB;QAEtB,MAAM,SAAS,GAAG,OAAO,GAAG,CAAC,gBAAgB,CAAC,KAAK,QAAQ,IAAI,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAW,CAAC,CAAC;QACxH,MAAM,WAAW,GAAG,IAAI,CAAC,4BAA4B,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QACxE,MAAM,WAAW,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;QACpC,MAAM,UAAU,GAAG,WAAW,IAAI,WAAW,CAAC,WAAW,CAAC,IAAI,IAAI;YAC9D,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;YAClC,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,IAAI,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACvD,OAAO;YACH,UAAU,EAAE,UAAU;YACtB,UAAU,EAAE,GAAG,CAAC,IAAI;YACpB,MAAM,EAAE,WAAW,EAAE,sEAAsE;YAC3F,SAAS,EAAE,SAAS;SACvB,CAAC;IACN,CAAC;IAED,oEAAoE;IAEpE,8FAA8F;IACtF,KAAK,CAAC,sBAAsB,CAChC,kBAA8C,EAC9C,WAAqB;QAErB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,kBAAkB,EAAE,WAAW,CAAC,CAAC;QACtE,MAAM,MAAM,GAAG,wIAAwI,CAAC;QACxJ,IAAI,GAAG,GAAuB,GAAG,IAAI,CAAC,UAAU,sBAAsB,MAAM,EAAE,CAAC;QAC/E,MAAM,GAAG,GAA8B,EAAE,CAAC;QAC1C,OAAO,GAAG,EAAE,CAAC;YACT,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;YACvF,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;gBAClD,MAAM,IAAI,KAAK,CAAC,2CAA2C,QAAQ,CAAC,MAAM,MAAM,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACvH,CAAC;YACD,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAwD,CAAC;YAC/E,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK,IAAI,EAAE;gBAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC9C,GAAG,GAAG,OAAO,IAAI,CAAC,iBAAiB,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC5F,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED,2GAA2G;IACnG,KAAK,CAAC,qBAAqB,CAC/B,kBAA8C,EAC9C,UAAkB,EAClB,WAAqB;QAErB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,kBAAkB,EAAE,WAAW,CAAC,CAAC;QACtE,MAAM,WAAW,GAAG,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,EAAE,UAAU,CAAC,CAAC;QAC5E,MAAM,MAAM,GAAG,kLAAkL,CAAC;QAClM,MAAM,MAAM,GAAG,wIAAwI,CAAC;QACxJ,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,UAAU,mCAAmC,kBAAkB,CAAC,WAAW,CAAC,MAAM,MAAM,IAAI,MAAM,EAAE,CAAC;QACzH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;QACvF,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QACzC,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;YAClD,MAAM,IAAI,KAAK,CAAC,yCAAyC,UAAU,WAAW,QAAQ,CAAC,MAAM,MAAM,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC1I,CAAC;QACD,OAAO,QAAQ,CAAC,IAA+B,CAAC;IACpD,CAAC;IAED,mGAAmG;IAC3F,kBAAkB,CAAC,kBAA8C,EAAE,UAAkB;QACzF,MAAM,GAAG,GAAG,IAAI,CAAC,qBAAqB,CAAC,kBAAkB,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;QACrF,IAAI,GAAG,EAAE,CAAC;YACN,MAAM,OAAO,GAAG,IAAI,CAAC,sBAAsB,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;YAChE,IAAI,OAAO;gBAAE,OAAO,OAAO,CAAC;QAChC,CAAC;QACD,OAAO,UAAU,CAAC;IACtB,CAAC;IAED,oEAAoE;IAC5D,4BAA4B,CAAC,CAA0B;QAC3D,MAAM,IAAI,GAAG,CAAC,CAAC,WAAqB,CAAC;QACrC,OAAO;YACH,IAAI,EAAE,IAAI;YACV,KAAK,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,IAAI;YACjD,WAAW,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,SAAS;YAC5D,uBAAuB,EAAE,CAAC,CAAC,qBAAqB,KAAK,IAAI,IAAI,IAAI,EAAE,uCAAuC;YAC1G,aAAa,EAAE,IAAI,EAAE,oFAAoF;SAC5G,CAAC;IACN,CAAC;IAED,uEAAuE;IAC/D,8BAA8B,CAAC,CAA6B,EAAE,SAAiB;QACnF,MAAM,IAAI,GAAG,CAAC,CAAC,WAAqB,CAAC;QACrC,MAAM,IAAI,GAAG,CAAC,CAAC,WAAW,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS,CAAC;QAC1D,MAAM,aAAa,GAAG,CAAC,CAAC,aAAa,EAAE,KAAK,CAAC;QAC7C,MAAM,UAAU,GAAG,aAAa,KAAK,gBAAgB,IAAI,aAAa,KAAK,qBAAqB,CAAC;QACjG,2FAA2F;QAC3F,sFAAsF;QACtF,MAAM,QAAQ,GAAG,CAAC,CAAC,aAAa,KAAK,QAAQ,IAAI,CAAC,CAAC,aAAa,KAAK,UAAU,IAAI,CAAC,CAAC,aAAa,KAAK,OAAO,CAAC;QAC/G,MAAM,YAAY,GAAG,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC1G,OAAO;YACH,IAAI,EAAE,IAAI;YACV,KAAK,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,IAAI;YACjD,WAAW,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,SAAS;YAC5D,QAAQ,EAAE,CAAC,CAAC,aAAa,IAAI,QAAQ;YACrC,UAAU,EAAE,UAAU;YACtB,WAAW,EAAE,IAAI;YACjB,YAAY,EAAE,IAAI;YAClB,UAAU,EAAE,CAAC,CAAC,gBAAgB,KAAK,KAAK,IAAI,CAAC,CAAC,gBAAgB,KAAK,KAAK;YACxE,YAAY,EAAE,YAAY,IAAI,IAAI;YAClC,gBAAgB,EAAE,YAAY;YAC9B,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;YAC/D,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;SAClE,CAAC;IACN,CAAC;IAED,oEAAoE;IAC5D,cAAc,CAAC,KAA4E;QAC/F,MAAM,IAAI,GAAG,KAAK,EAAE,kBAAkB,EAAE,KAAK,CAAC;QAC9C,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAC1E,CAAC;IAED,oEAAoE;IAEpE,mGAAmG;IAC3F,KAAK,CAAC,WAAW,CACrB,kBAA8C,EAC9C,WAAsB;QAEtB,IAAI,kBAAkB,CAAC,YAAY,EAAE,CAAC;YAClC,OAAO,IAAI,CAAC,yBAAyB,CAAC,kBAAkB,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;QACxF,CAAC;QACD,IAAI,kBAAkB,CAAC,aAAa,EAAE,CAAC;YACnC,OAAO,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAC,CAAC;QAC7E,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,yGAAyG,CAAC,CAAC;IAC/H,CAAC;IAED,2EAA2E;IACnE,KAAK,CAAC,yBAAyB,CACnC,YAAoB,EACpB,WAAsB,EACtB,QAA4B;QAE5B,MAAM,EAAE,GAAG,QAAQ,IAAI,IAAI,QAAQ,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,eAAe,CAAqB,iBAAiB,EAAE,WAAW,CAAC,CAAC;QAC1F,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC7C,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;QACrF,CAAC;QACD,OAAO,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IACxD,CAAC;IAED,+FAA+F;IACvF,cAAc,CAAC,GAAY;QAC/B,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;QACpE,CAAC;QACD,MAAM,GAAG,GAAG,GAA8B,CAAC;QAC3C,MAAM,MAAM,GAAG,CAAC,GAAG,IAAc,EAAsB,EAAE;YACrD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;gBACrB,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;gBAChC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;oBACvC,IAAI,CAAC,CAAC,WAAW,EAAE,KAAK,KAAK,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC;wBAAE,OAAO,CAAC,CAAC;gBACrF,CAAC;YACL,CAAC;YACD,OAAO,SAAS,CAAC;QACrB,CAAC,CAAC;QACF,MAAM,MAAM,GAAG,CAAC,GAAG,IAAc,EAAsB,EAAE;YACrD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;gBACrB,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;gBAChC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;oBACvC,IAAI,CAAC,CAAC,WAAW,EAAE,KAAK,KAAK,IAAI,OAAO,CAAC,KAAK,QAAQ;wBAAE,OAAO,CAAC,CAAC;gBACrE,CAAC;YACL,CAAC;YACD,OAAO,SAAS,CAAC;QACrB,CAAC,CAAC;QAEF,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC;QAC3D,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,OAAO,CAAC,CAAC;QAC3E,MAAM,YAAY,GAAG,MAAM,CAAC,cAAc,EAAE,eAAe,EAAE,QAAQ,CAAC,CAAC;QACvE,MAAM,cAAc,GAAG,MAAM,CAAC,gBAAgB,EAAE,iBAAiB,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;QAE3H,IAAI,CAAC,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;QAC1F,IAAI,CAAC,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;QAC1F,IAAI,CAAC,YAAY;YAAE,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;QAClG,IAAI,CAAC,cAAc;YAAE,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;QAEtG,OAAO;YACH,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,QAAQ;YAClB,YAAY,EAAE,YAAY;YAC1B,cAAc,EAAE,cAAc,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;YAClD,UAAU,EAAE,MAAM,CAAC,YAAY,EAAE,aAAa,CAAC,IAAI,mBAAmB;YACtE,aAAa,EAAE,MAAM,CAAC,eAAe,EAAE,gBAAgB,EAAE,WAAW,CAAC;YACrE,KAAK,EAAE,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC;YAChC,UAAU,EAAE,MAAM,CAAC,YAAY,CAAC,IAAI,mBAAmB;YACvD,gBAAgB,EAAE,MAAM,CAAC,kBAAkB,CAAC,IAAI,0BAA0B;YAC1E,oBAAoB,EAAE,MAAM,CAAC,sBAAsB,CAAC,IAAI,CAAC;YACzD,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,mBAAmB,EAAE,mBAAmB,CAAC;SAC3F,CAAC;IACN,CAAC;IAED,uEAAuE;IAC/D,cAAc,CAAC,MAAgC;QACnD,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,IAAI,mBAAmB,CAAC;QACzD,OAAO,GAAG,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,aAAa,OAAO,EAAE,CAAC;IAC9E,CAAC;IAED,oEAAoE;IAE5D,KAAK,CAAC,iBAAiB,CAC3B,GAAW,EACX,MAAc,EACd,OAA+B,EAC/B,IAAa,EACb,SAAiB;QAEjB,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,aAAa,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC,CAAC;QACtE,IAAI,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAC9B,MAAM;gBACN,OAAO;gBACP,IAAI,EAAE,IAAI,KAAK,SAAS,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,QAAQ;oBAC/D,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;oBACtB,CAAC,CAAC,SAAS;gBACf,MAAM,EAAE,UAAU,CAAC,MAAM;aAC5B,CAAC,CAAC;YACH,MAAM,eAAe,GAAG,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;YAC9D,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC;YAC1D,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC;QACnF,CAAC;gBAAS,CAAC;YACP,YAAY,CAAC,aAAa,CAAC,CAAC;QAChC,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,iBAAiB,CAAC,QAAkB;QAC9C,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC;QACvB,IAAI,CAAC;YACD,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC;QACvC,CAAC;QAAC,MAAM,CAAC;YACL,OAAO,IAAI,CAAC;QAChB,CAAC;IACL,CAAC;IAEO,cAAc,CAAC,OAAgB;QACnC,MAAM,GAAG,GAA2B,EAAE,CAAC;QACvC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAC3B,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,GAAG,KAAK,CAAC;QACnC,CAAC,CAAC,CAAC;QACH,OAAO,GAAG,CAAC;IACf,CAAC;IAEO,WAAW,CAAC,QAAsB;QACtC,OAAO,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,CAAC;IACzF,CAAC;IAEO,mBAAmB,CAAC,OAAe,EAAE,gBAAyB;QAClE,IAAI,gBAAgB,EAAE,CAAC;YACnB,MAAM,MAAM,GAAG,QAAQ,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC;YAC9C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtC,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC;YAC5C,CAAC;QACL,CAAC;QACD,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;IACzD,CAAC;IAEO,KAAK,CAAC,EAAU;QACpB,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;IAC3D,CAAC;IAED,oEAAoE;IAEpE;;;;;OAKG;IACK,oBAAoB,CAAC,GAAW;QACpC,OAAO,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAEO,iBAAiB,CAAC,OAAe,EAAE,OAAe;QACtD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACzC,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC;QAC/D,OAAO,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC;IAC5B,CAAC;IAEO,WAAW,CAAC,IAAa;QAC7B,MAAM,CAAC,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACjE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACnC,CAAC;IAKD;;;;OAIG;IACK,kBAAkB,CAAC,UAAkB;QACzC,IAAI,CAAC,IAAI,CAAC,iBAAiB;YAAE,OAAO,IAAI,CAAC;QACzC,OAAO,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,iBAAiB,EAAE,UAAU,CAAC,CAAC;IAC1E,CAAC;IAEO,qBAAqB,CAAC,aAAqB,EAAE,UAAkB;QACnE,IAAI,CAAC,aAAa;YAAE,OAAO,IAAI,CAAC;QAChC,IAAI,CAAC;YACD,OAAO,IAAI,CAAC,eAAe,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;QAC3D,CAAC;QAAC,MAAM,CAAC;YACL,OAAO,IAAI,CAAC;QAChB,CAAC;IACL,CAAC;IAED,0FAA0F;IAClF,sBAAsB,CAAC,GAA8B,EAAE,GAAW;QACtE,MAAM,GAAG,GAAI,GAAoD,CAAC,aAAa,CAAC;QAChF,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACjD,IAAI,CAAC;YACD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA4B,CAAC;YACvD,MAAM,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;YACnB,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QAC1E,CAAC;QAAC,MAAM,CAAC;YACL,OAAO,IAAI,CAAC;QAChB,CAAC;IACL,CAAC;IAED,uGAAuG;IAC/F,uBAAuB,CAAC,KAAc;QAC1C,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YACrC,MAAM,CAAC,GAAG,KAA+E,CAAC;YAC1F,OAAO,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC;QAClC,CAAC;QACD,OAAO,SAAS,CAAC;IACrB,CAAC;CACJ,CAAA;AA91BY,0BAA0B;IADtC,aAAa,CAAC,wBAAwB,EAAE,4DAA4D,CAAC;GACzF,0BAA0B,CA81BtC;;AAED,uFAAuF;AACvF,MAAM,UAAU,8BAA8B,KAAuB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from './DynamicsDataverseConnector.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 './DynamicsDataverseConnector.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,iCAAiC,CAAC;AAEhD;oGACoG;AACpG,MAAM,UAAU,iBAAiB,KAAiD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@memberjunction/connector-microsoft-dynamics-365-dataverse",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MemberJunction DynamicsDataverse 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 --passWithNoTests"
|
|
14
|
+
},
|
|
15
|
+
"author": "MemberJunction.com",
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@memberjunction/core": ">=5.42.0 <6.0.0",
|
|
19
|
+
"@memberjunction/core-entities": ">=5.42.0 <6.0.0",
|
|
20
|
+
"@memberjunction/global": ">=5.42.0 <6.0.0",
|
|
21
|
+
"@memberjunction/integration-engine": ">=5.42.0 <6.0.0"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "24.10.11",
|
|
26
|
+
"tsc-alias": "^1.8.16",
|
|
27
|
+
"typescript": "^5.9.3",
|
|
28
|
+
"vitest": "^4.0.18",
|
|
29
|
+
"@memberjunction/core": "^5.42.0",
|
|
30
|
+
"@memberjunction/core-entities": "^5.42.0",
|
|
31
|
+
"@memberjunction/global": "^5.42.0",
|
|
32
|
+
"@memberjunction/integration-engine": "^5.42.0"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/MemberJunction/Integrations"
|
|
37
|
+
}
|
|
38
|
+
}
|