@memberjunction/connector-salesforce 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,460 @@
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 DefaultFieldMapping, type DefaultIntegrationConfig, type ExternalRecord, type CRUDResult, type CreateRecordContext, type UpdateRecordContext, type DeleteRecordContext, type GetRecordContext, type SearchContext, type SearchResult, type SourceSchemaInfo, type IntegrationObjectInfo, type ActionGeneratorConfig, type IntrospectSchemaOptions } from '@memberjunction/integration-engine';
4
+ /** Supported Salesforce auth flows */
5
+ type SalesforceAuthFlow = 'jwt_bearer' | 'client_credentials';
6
+ /** Connection configuration parsed from CompanyIntegration.Configuration JSON */
7
+ export interface SalesforceConnectionConfig {
8
+ /** Auth flow to use */
9
+ AuthFlow: SalesforceAuthFlow;
10
+ /** Salesforce login URL (e.g. https://login.salesforce.com or https://myorg.my.salesforce.com) */
11
+ LoginUrl: string;
12
+ /** Connected App Consumer Key */
13
+ ClientId: string;
14
+ /** Salesforce REST API version. Default: '61.0' */
15
+ ApiVersion: string;
16
+ /** JWT Bearer only — Integration user email */
17
+ Username?: string;
18
+ /** JWT Bearer only — PEM-encoded RSA private key */
19
+ PrivateKey?: string;
20
+ /** Client Credentials only — Connected App client secret */
21
+ ClientSecret?: string;
22
+ /** Optional — token endpoint URL if different from LoginUrl (e.g. My Domain URL for JWT flow) */
23
+ TokenUrl?: string;
24
+ /** Maximum retries for rate-limited or failed requests. Default: 5 */
25
+ MaxRetries?: number;
26
+ /** HTTP request timeout in milliseconds. Default: 30000 */
27
+ RequestTimeoutMs?: number;
28
+ /** Minimum milliseconds between API requests. Default: 100 */
29
+ MinRequestIntervalMs?: number;
30
+ /** Batch size for SOQL queries. Default: 2000 */
31
+ DefaultBatchSize?: number;
32
+ }
33
+ /** Extended auth context carrying Salesforce credentials and cached token */
34
+ interface SalesforceAuthContext extends RESTAuthContext {
35
+ /** Salesforce instance URL (pod-specific, e.g., https://na1.salesforce.com) */
36
+ InstanceUrl: string;
37
+ /** API version string */
38
+ ApiVersion: string;
39
+ /** Full config for reference */
40
+ Config: SalesforceConnectionConfig;
41
+ /** The CompanyIntegration this auth was built for — lets API URLs route through the
42
+ * overridable GetBaseURL() (production returns InstanceUrl; test harnesses redirect to a mock). */
43
+ CompanyIntegration: MJCompanyIntegrationEntity;
44
+ }
45
+ /**
46
+ * Production Salesforce CRM connector using the Salesforce REST API v61.0.
47
+ *
48
+ * Supports:
49
+ * - OAuth 2.0 JWT Bearer Token authentication (RS256)
50
+ * - Live schema discovery via SF Describe API
51
+ * - SOQL-based incremental sync with SystemModstamp watermarks
52
+ * - Full CRUD (Create, Update, Delete) via SObject REST API
53
+ * - SOQL/SOSL search support
54
+ * - Comprehensive SF error code handling and governor limit management
55
+ *
56
+ * Extends BaseRESTIntegrationConnector but overrides FetchChanges entirely
57
+ * because Salesforce uses SOQL queries instead of standard REST list endpoints.
58
+ */
59
+ export declare class SalesforceConnector extends BaseRESTIntegrationConnector {
60
+ private cachedAuth;
61
+ private tokenObtainedAt;
62
+ private lastRequestTime;
63
+ private governorState;
64
+ private _config;
65
+ private get effectiveMaxRetries();
66
+ private get effectiveRequestTimeoutMs();
67
+ private get effectiveMinRequestIntervalMs();
68
+ private get effectiveBatchSize();
69
+ get SupportsCreate(): boolean;
70
+ get SupportsUpdate(): boolean;
71
+ get SupportsDelete(): boolean;
72
+ get SupportsSearch(): boolean;
73
+ get IntegrationName(): string;
74
+ /**
75
+ * CORRECTION (IMPROVE build): affirm AUTHORITATIVE discovery. The global describe
76
+ * (`/sobjects/`) returns the COMPLETE credentialed gamut of queryable objects this
77
+ * org exposes (standard + custom `__c`), and `DoIntrospectSchema` describes that whole
78
+ * set. So an object/field absent from a comprehensive refresh genuinely means the
79
+ * source dropped it, and the engine's refresh path may safely DEACTIVATE it
80
+ * (`Status='Disabled'` — reversible; it flips back to Active if the object reappears on
81
+ * a later discovery). This is what makes comprehensive-refresh deactivation correct
82
+ * for Salesforce rather than wrongly wiping Declared metadata.
83
+ */
84
+ get DiscoveryIsAuthoritative(): boolean;
85
+ /**
86
+ * Action-generation hint set. CORRECTION (IMPROVE build): this used to return
87
+ * a baked famous-subset catalog (~9 objects hardcoded in this file). That is a
88
+ * `catalog-in-code` defect — it froze the object universe to a famous subset.
89
+ *
90
+ * It now derives the hint set ENTIRELY from the runtime-cached IntegrationObject /
91
+ * IntegrationObjectField metadata for the Salesforce integration — the FULL
92
+ * Declared (credential-free catalog) + Discovered (live `DiscoverObjects` describe,
93
+ * custom `__c` included) gamut. There is NO baked catalog. If the metadata cache is
94
+ * not yet populated (e.g. action generation runs before the integration is seeded),
95
+ * it returns an empty array — the connector NEVER falls back to a hardcoded list.
96
+ *
97
+ * The per-tenant object UNIVERSE for sync comes exclusively from `DiscoverObjects`
98
+ * (live global describe / sObjects endpoint); this method only shapes the cached
99
+ * catalog into the ActionMetadataGenerator's hint structure.
100
+ */
101
+ GetIntegrationObjects(): IntegrationObjectInfo[];
102
+ GetActionGeneratorConfig(): ActionGeneratorConfig | null;
103
+ TestConnection(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<ConnectionTestResult>;
104
+ /**
105
+ * Discovers available objects by calling the SF SObjects API.
106
+ * Returns user-relevant queryable objects: standard CRM objects + custom
107
+ * (__c). Filters out audit/system noise (~1,866 → ~150-300 typically).
108
+ * Set MJ_SALESFORCE_INCLUDE_ALL_SOBJECTS=true to bypass the filter and
109
+ * return the full catalog (useful for debugging or unusual integrations).
110
+ */
111
+ DiscoverObjects(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<ExternalObjectSchema[]>;
112
+ /**
113
+ * Heuristic for "user-relevant" SF object: customer data (custom objects
114
+ * with __c suffix) OR createable standard CRM objects, excluding audit
115
+ * tables, system metadata, and managed-package telemetry.
116
+ *
117
+ * Standard SF orgs return ~1,866 sobjects from describeGlobal — most are
118
+ * audit/internal noise that don't represent business data the user wants
119
+ * to sync into MJ. This drops them to ~150-300.
120
+ */
121
+ private isUserRelevantSObject;
122
+ /**
123
+ * Discovers fields on a specific SF object via the Describe API.
124
+ * Skips compound fields (address, location) — their component fields are included individually.
125
+ */
126
+ DiscoverFields(companyIntegration: MJCompanyIntegrationEntity, objectName: string, contextUser: UserInfo): Promise<ExternalFieldSchema[]>;
127
+ /**
128
+ * Full schema introspection — builds object graph with relationships.
129
+ *
130
+ * If `options.ObjectNames` is provided, only those objects are described
131
+ * (fast path for user-selected subsets). Without a filter, every
132
+ * queryable sobject is described — ~70s even with parallelism on a
133
+ * large org — and the result is cached per-org for 5 minutes so
134
+ * back-to-back resolver calls don't re-describe.
135
+ *
136
+ * Filtered calls bypass the cache on purpose: the subset is typically
137
+ * small and cheap, and the full-schema cache would shadow newer results
138
+ * if a user selects a previously-unknown object.
139
+ */
140
+ IntrospectSchema(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo, options?: IntrospectSchemaOptions): Promise<SourceSchemaInfo>;
141
+ private DoIntrospectSchema;
142
+ private static readonly INTROSPECT_CACHE_TTL_MS;
143
+ private static readonly introspectCache;
144
+ /**
145
+ * Fetches changed records using SOQL queries with SystemModstamp watermarks.
146
+ * Completely overrides the base class REST pagination because SF uses
147
+ * SOQL + queryMore, not standard REST list endpoints.
148
+ *
149
+ * Dispatches to family-specific fetch routines based on the
150
+ * IntegrationObject metadata's `DefaultQueryParams.api_family` hint:
151
+ * tooling → Tooling API, analytics_report/dashboard → Analytics API,
152
+ * bulk_* / composite → not intended for listing (returns empty batch),
153
+ * everything else → standard SObject SOQL flow.
154
+ */
155
+ FetchChanges(ctx: FetchContext): Promise<FetchBatchResult>;
156
+ /**
157
+ * Resolves the API family for a given IntegrationObject by reading its
158
+ * `DefaultQueryParams.api_family` flag from the engine cache. Defaults to
159
+ * `sobject` when no metadata is available (common in unit tests).
160
+ */
161
+ private ResolveAPIFamily;
162
+ private IsValidFamily;
163
+ /**
164
+ * Returns the REST path prefix for an SObject in the given API family.
165
+ * Used by CRUD operations and field discovery.
166
+ */
167
+ private SObjectBasePath;
168
+ /**
169
+ * Returns the SOQL query endpoint for the given API family.
170
+ * Standard uses /query + /queryAll; Tooling uses /tooling/query.
171
+ */
172
+ private SOQLEndpoint;
173
+ /**
174
+ * Creates a new record in Salesforce. Dispatches to the appropriate API
175
+ * family endpoint (standard SObjects, Tooling, Bulk Ingest/Query jobs, or
176
+ * Composite) based on the IntegrationObject metadata.
177
+ */
178
+ CreateRecord(ctx: CreateRecordContext): Promise<CRUDResult>;
179
+ /**
180
+ * Updates an existing record in Salesforce (PATCH — only changed fields).
181
+ * Honors API family routing; Tooling uses `/tooling/sobjects/`, everything
182
+ * else uses standard `/sobjects/`.
183
+ */
184
+ UpdateRecord(ctx: UpdateRecordContext): Promise<CRUDResult>;
185
+ /**
186
+ * Deletes a record from Salesforce. For Bulk Job families, delete aborts
187
+ * the job. For standard and Tooling SObjects, performs a soft delete
188
+ * (Recycle Bin, 15-day retention). Analytics/Composite/Knowledge do not
189
+ * support deletion through this path.
190
+ */
191
+ DeleteRecord(ctx: DeleteRecordContext): Promise<CRUDResult>;
192
+ /**
193
+ * Retrieves a single record by its Salesforce ID. Routes to the API family
194
+ * endpoint indicated by IntegrationObject metadata.
195
+ */
196
+ GetRecord(ctx: GetRecordContext): Promise<ExternalRecord | null>;
197
+ /**
198
+ * Searches records using SOQL WHERE clauses built from the provided filters.
199
+ * Search is supported for Standard SObjects and Tooling SObjects; other API
200
+ * families return an empty result.
201
+ */
202
+ SearchRecords(ctx: SearchContext): Promise<SearchResult>;
203
+ GetDefaultFieldMappings(objectName: string, _entityName: string): DefaultFieldMapping[];
204
+ GetDefaultConfiguration(): DefaultIntegrationConfig;
205
+ protected Authenticate(companyIntegration: MJCompanyIntegrationEntity, contextUser: UserInfo): Promise<SalesforceAuthContext>;
206
+ protected BuildHeaders(auth: RESTAuthContext): Record<string, string>;
207
+ protected MakeHTTPRequest(auth: RESTAuthContext, url: string, method: string, headers: Record<string, string>, body?: unknown): Promise<RESTResponse>;
208
+ protected NormalizeResponse(rawBody: unknown, _responseDataKey: string | null): Record<string, unknown>[];
209
+ /**
210
+ * CORRECTION (IMPROVE build): per-record transform that strips Salesforce's
211
+ * `attributes` metadata blob (e.g. `{ type, url }`) from every record. This is a
212
+ * SANCTIONED removal declared in {@link ExcludedSourceKeys}, NOT a silent drop — the
213
+ * `attributes` key is vendor envelope noise, not customer data, so dropping it can
214
+ * never lose a custom column. Every OTHER source field flows through untouched, so
215
+ * the full-record pass-through contract (custom-column capture) is preserved.
216
+ *
217
+ * Used by both the base-fetch path (GetRecord via applyTransformPreservingKeys) and
218
+ * the SOQL override path (RawToExternalRecord routes through StripVendorAttributes,
219
+ * which reuses ExcludedSourceKeys so the two paths agree on what is removed).
220
+ */
221
+ protected TransformRecord(raw: Record<string, unknown>, _obj: MJIntegrationObjectEntity, _fields: MJIntegrationObjectFieldEntity[]): Record<string, unknown>;
222
+ /**
223
+ * The Salesforce `attributes` blob is the ONLY key any object's TransformRecord
224
+ * removes. Declaring it here makes the removal auditable and excludes it from the
225
+ * base's re-add-dropped-keys safety net (and from change-detection).
226
+ */
227
+ protected ExcludedSourceKeys(_objectName: string): string[];
228
+ protected ExtractPaginationInfo(rawBody: unknown, _paginationType: PaginationType, _currentPage: number, _currentOffset: number, _pageSize: number): PaginationState;
229
+ protected GetBaseURL(_companyIntegration: MJCompanyIntegrationEntity, auth: RESTAuthContext): string;
230
+ /**
231
+ * The API origin for all REST calls. Routes through GetBaseURL() so a test harness can
232
+ * redirect the connector to a mock server (the base class hooks GetBaseURL); in production
233
+ * this returns the authenticated InstanceUrl unchanged. ALL URL construction must go through
234
+ * this rather than reading auth.InstanceUrl directly, or the connector is not mock-testable.
235
+ */
236
+ private ApiBase;
237
+ /**
238
+ * Builds a SOQL query string with optional watermark filter.
239
+ */
240
+ private BuildSOQLQuery;
241
+ /**
242
+ * Executes a SOQL query and returns records as a FetchBatchResult.
243
+ * For the standard SObject family this uses `queryAll` so that soft-deleted
244
+ * records are returned (IsDeleted=true). For the Tooling family, the
245
+ * Tooling API's `/tooling/query` endpoint is used — `queryAll` is not
246
+ * supported there.
247
+ */
248
+ private ExecuteSOQLQuery;
249
+ /**
250
+ * Fetches the next page of results using a queryMore locator.
251
+ */
252
+ private FetchNextPage;
253
+ /**
254
+ * Parses a SOQL query response into a FetchBatchResult.
255
+ */
256
+ private ParseSOQLResponse;
257
+ /**
258
+ * Extracts the maximum SystemModstamp value from a batch of records and
259
+ * advances it by 1ms so the next sync's `WHERE SystemModstamp >= <wm>`
260
+ * filter excludes the boundary cluster.
261
+ *
262
+ * Why advance: the SOQL filter uses `>=` (not `>`) on purpose so we don't
263
+ * drop records that share the modstamp instant of the previous max. But
264
+ * if the saved watermark equals the new max, every subsequent incremental
265
+ * re-pulls the same boundary cluster forever and the watermark plateaus.
266
+ * Observed in the wild: 1,800 EmailMessage records bulk-imported with
267
+ * identical SystemModstamp re-fetched on every run with `Updated` count
268
+ * never decreasing.
269
+ *
270
+ * SF's SystemModstamp is millisecond-precision, so adding 1ms cannot
271
+ * skip a real record — there is nothing scheduled between `max` and
272
+ * `max + 1ms`. The connector returns the advanced value as
273
+ * `NewWatermarkValue`; the engine persists it; the next run picks up
274
+ * cleanly past the cluster.
275
+ */
276
+ private ExtractMaxWatermark;
277
+ /**
278
+ * Gets queryable field names for a SF object via the Describe API. When
279
+ * `family === 'tooling'`, calls the Tooling describe endpoint instead of
280
+ * the standard one.
281
+ */
282
+ private GetQueryableFieldNames;
283
+ /**
284
+ * Builds a signed JWT assertion for the SF JWT Bearer Token flow.
285
+ */
286
+ private BuildJWTAssertion;
287
+ /**
288
+ * Normalize a PEM private key that may have had its newlines stripped or
289
+ * replaced with spaces / literal "\n" sequences. Rebuilds the standard
290
+ * BEGIN header / 64-char body / END footer layout that OpenSSL requires.
291
+ */
292
+ private NormalizePem;
293
+ /**
294
+ * Exchanges a JWT assertion for an access token at the SF token endpoint.
295
+ */
296
+ private ExchangeJWTForToken;
297
+ /**
298
+ * Exchanges Client Credentials for an access token.
299
+ * Used with Connected Apps configured for the OAuth 2.0 Client Credentials flow.
300
+ */
301
+ private ExchangeClientCredentialsForToken;
302
+ /**
303
+ * Checks if the cached token is still valid (within 80% of lifetime).
304
+ */
305
+ private IsTokenValid;
306
+ /**
307
+ * Loads credentials from the Credential entity or Configuration JSON.
308
+ */
309
+ private LoadCredentials;
310
+ private LoadFromCredentialEntity;
311
+ private ParseCredentialJson;
312
+ private BuildConnectionConfig;
313
+ private ApplyConfigOverrides;
314
+ /**
315
+ * Throttles requests to respect minimum interval between API calls.
316
+ */
317
+ private ThrottleIfNeeded;
318
+ /**
319
+ * Checks governor limits and pauses if approaching the daily API limit.
320
+ */
321
+ private CheckGovernorLimits;
322
+ /**
323
+ * Parses the Sforce-Limit-Info header from every SF response.
324
+ */
325
+ private ParseGovernorLimits;
326
+ private FetchWithTimeout;
327
+ private BuildRESTResponse;
328
+ private ExtractHeaders;
329
+ private SafeParseJSON;
330
+ private CalculateRetryDelay;
331
+ /**
332
+ * Parses the vendor's stated retry wait from a 429/503 response into milliseconds, so the connector
333
+ * honors `Retry-After` (delta-seconds OR an HTTP-date) and Salesforce's `Sforce-Limit-Info` rather
334
+ * than blindly using exponential backoff. Returns null when no usable signal is present (caller falls
335
+ * back to {@link CalculateRetryDelay}). Capped at 60s so a hostile/garbage header can't stall a sync.
336
+ */
337
+ private RetryAfterMs;
338
+ private Sleep;
339
+ /**
340
+ * Maps a Salesforce field describe to an ExternalFieldSchema.
341
+ * Custom fields can be detected by callers via the `__c` suffix in the
342
+ * field name — Salesforce standardizes this convention for all custom
343
+ * fields on both standard and custom SObjects.
344
+ */
345
+ private MapSFFieldToSchema;
346
+ /**
347
+ * Maps a Salesforce field type to a generic integration type.
348
+ */
349
+ private MapSalesforceType;
350
+ /**
351
+ * Formats a watermark value as a SOQL datetime literal.
352
+ */
353
+ private FormatSOQLDateTime;
354
+ /**
355
+ * Builds a SOQL WHERE clause from a Filters map.
356
+ */
357
+ private BuildWhereClauseFromFilters;
358
+ /**
359
+ * Checks if a response contains a specific SF error code.
360
+ */
361
+ private HasSFErrorCode;
362
+ /**
363
+ * Extracts SF error objects from a response.
364
+ */
365
+ private ExtractSFErrors;
366
+ /**
367
+ * Checks if a response body contains an UNABLE_TO_LOCK_ROW error.
368
+ */
369
+ private IsLockRowError;
370
+ /**
371
+ * Builds a CRUDResult for error responses.
372
+ */
373
+ private BuildCRUDError;
374
+ /**
375
+ * Handles write errors with special retry logic for UNABLE_TO_LOCK_ROW.
376
+ */
377
+ private HandleWriteError;
378
+ /**
379
+ * Converts a raw SF API record to an ExternalRecord. The SOQL override path
380
+ * hand-builds records, so it routes through the SAME sanctioned strip the base path
381
+ * uses: every key flows through to `Fields` EXCEPT those declared in
382
+ * {@link ExcludedSourceKeys} (just `attributes`). This preserves full-record
383
+ * pass-through for custom-column capture while removing only the vendor envelope blob.
384
+ */
385
+ private RawToExternalRecord;
386
+ /**
387
+ * Strips read-only and system fields from an attributes map before write operations.
388
+ */
389
+ private StripReadOnlyFields;
390
+ private BuildSourceObjectFromDescribe;
391
+ private GetContactMappings;
392
+ private GetAccountMappings;
393
+ private GetLeadMappings;
394
+ private ValidateResponse;
395
+ private PreviewBody;
396
+ /**
397
+ * Lists Analytics Reports or Dashboards via the Analytics API. Read-only;
398
+ * no incremental-sync support at this endpoint.
399
+ */
400
+ private FetchAnalyticsList;
401
+ private AnalyticsItemToRecord;
402
+ private GetAnalyticsReport;
403
+ private GetAnalyticsDashboard;
404
+ /**
405
+ * Fetches published Knowledge article versions. This endpoint is
406
+ * paginated via `pageNumber`/`pageSize` (not cursor). The connector
407
+ * returns a single batch; callers that need pagination can issue
408
+ * additional requests via SearchRecords.
409
+ */
410
+ private FetchKnowledgeArticles;
411
+ /**
412
+ * Lists in-flight Bulk API 2.0 jobs. Useful for monitoring background
413
+ * imports/queries the integration previously started.
414
+ */
415
+ private FetchBulkJobs;
416
+ /**
417
+ * Creates (starts) a new Bulk API 2.0 ingest or query job using the
418
+ * provided attributes. Required fields differ by operation; we pass
419
+ * attributes straight through to Salesforce after stripping any readonly
420
+ * fields. For ingest, CSV data is uploaded in a separate request (PUT) —
421
+ * callers must issue that themselves via UploadBulkData once the job ID
422
+ * is known.
423
+ */
424
+ private CreateBulkJob;
425
+ /**
426
+ * Aborts a running Bulk API 2.0 job by PATCHing state=Aborted.
427
+ */
428
+ private AbortBulkJob;
429
+ private GetBulkJob;
430
+ /**
431
+ * Executes a composite request — up to 25 sub-requests in one HTTP call.
432
+ * The `attrs` payload is expected to be the full composite body or at
433
+ * least a `compositeRequest` array. Returns the overall composite
434
+ * response as the created record's ExternalID (Salesforce returns
435
+ * per-sub-request results; callers should inspect Fields for details).
436
+ *
437
+ * Salesforce returns HTTP 200 for the OVERALL composite call even when
438
+ * individual sub-requests fail (the composite envelope carries a
439
+ * per-sub-request `httpStatusCode`). Blanket-returning Success:true on the
440
+ * envelope status swallows those sub-request failures. We inspect the
441
+ * `compositeResponse` array and fail loudly if any sub-request returned a
442
+ * 4xx/5xx, summarizing the sub-request errors so the caller sees them.
443
+ */
444
+ private ExecuteCompositeRequest;
445
+ /**
446
+ * Inspects a composite response body for per-sub-request failures. A
447
+ * sub-request failed if its `httpStatusCode` is >= 400. Returns a
448
+ * human-readable summary string per failed sub-request (empty array when
449
+ * all sub-requests succeeded or the body has no compositeResponse array).
450
+ */
451
+ private CollectCompositeSubErrors;
452
+ /** Formats a single failed composite sub-response into a readable error string. */
453
+ private SummarizeCompositeSubError;
454
+ /**
455
+ * Salesforce sub-request error bodies are typically an array of
456
+ * `{ errorCode, message }`. Pull a concise detail string out of that shape.
457
+ */
458
+ private ExtractCompositeSubErrorDetail;
459
+ }
460
+ export {};