LCOV - code coverage report
Current view: top level - dashql-app/src/connections/salesforce - salesforce_api_client.ts Coverage Total Hit
Test: DashQL 915828b Lines: 97.1 % 414 402
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as connection from '@ankoh/dashql-jsonschema/connection.js';
       2              : import * as pb from "../../proto.js";
       3              : 
       4              : import { Logger } from '../../platform/logger/logger.js';
       5              : import { HttpClient } from '../../platform/http/http_client.js';
       6              : import { HyperDatabaseChannel, HyperQueryExecutionOptions, HyperQueryResultStream } from '../hyper/hyperdb_grpc_client.js';
       7              : import { BASE64URL_CODEC } from "../../utils/base64.js";
       8              : 
       9            1 : const LOG_CTX = "salesforce_api";
      10            1 : const SALESFORCE_API_VERSION = "v64.0";
      11            1 : export const DEFAULT_SALESFORCE_DATA_SPACE = "default";
      12            1 : export const SALESFORCE_METADATA_ENTITY_TYPES = ['DataModelObject', 'DataLakeObject', 'CalculatedInsight'] as const;
      13              : 
      14              : /// The Data Cloud auth infos
      15              : export interface SalesforceAuthInfo {
      16              :     /// The core tenant id
      17              :     coreTenantId: string | null;
      18              :     /// The core access token
      19              :     coreAccessToken: string | null;
      20              :     /// The offcore jwt
      21              :     offcoreRawJwt: string;
      22              :     /// The offcore instance url
      23              :     offcoreInstanceUrl: string | null;
      24              :     /// The offcore tenant id
      25              :     offcoreTenantId: string | null;
      26              :     /// The offcore access token
      27              :     offcoreAccessToken: string | null;
      28              :     /// The dataspace
      29              :     dataspace: string;
      30              : }
      31              : 
      32              : export interface SalesforceMetadataProgress {
      33              :     collection: string;
      34              :     page: number;
      35              :     loaded: number;
      36              :     total: number | null;
      37              :     state: 'requesting' | 'complete' | 'failed';
      38              :     error?: string;
      39              : }
      40              : 
      41            1 : export function formatSalesforceMetadataProgress(
      42            1 :     progressByCollection: ReadonlyMap<string, SalesforceMetadataProgress>,
      43            1 : ): string {
      44            1 :     const labels: Record<string, string> = {
      45            1 :         DataModelObject: 'DMO',
      46            1 :         DataLakeObject: 'DLO',
      47            1 :         CalculatedInsight: 'CI',
      48            1 :     };
      49            1 :     let completed = 0;
      50            1 :     let running = 0;
      51            1 :     let failed = 0;
      52            1 :     let pending = 0;
      53            1 :     const stages = SALESFORCE_METADATA_ENTITY_TYPES.map(collection => {
      54            1 :         const progress = progressByCollection.get(collection);
      55            1 :         if (progress?.state === 'complete') {
      56            1 :             completed += 1;
      57            1 :             return `${labels[collection]} ${progress.loaded}`;
      58            1 :         }
      59            1 :         if (progress?.state === 'failed') {
      60            1 :             failed += 1;
      61            1 :             return `${labels[collection]} failed`;
      62            1 :         }
      63            1 :         if (progress?.state === 'requesting') {
      64            1 :             running += 1;
      65            1 :             return `${labels[collection]} -`;
      66            1 :         }
      67            0 :         pending += 1;
      68            0 :         return `${labels[collection]} -`;
      69            1 :     });
      70            1 :     const counts = [`${completed} done`, `${running} active`, `${failed} failed`];
      71            1 :     if (pending > 0) counts.push(`${pending} pending`);
      72            1 :     return `Metadata: ${counts.join(', ')} | ${stages.join(' | ')}`;
      73            1 : }
      74              : 
      75            1 : export function getSalesforceDataSpace(access: connection.SalesforceDataCloudAccessToken): string {
      76            1 :     const customAttributes = access.jwt?.payload?.customAttributes as unknown;
      77            1 :     if (customAttributes && typeof customAttributes === "object") {
      78            1 :         const attributes = customAttributes as Record<string, unknown>;
      79            1 :         const nested = attributes.data;
      80            1 :         const dataSpace = nested && typeof nested === "object"
      81            1 :             ? (nested as Record<string, unknown>).dataspace
      82            1 :             : attributes.dataspace;
      83            1 :         if (typeof dataSpace === "string" && dataSpace.length > 0) {
      84            1 :             return dataSpace;
      85              :         }
      86            1 :     }
      87            1 :     return DEFAULT_SALESFORCE_DATA_SPACE;
      88            1 : }
      89              : 
      90              : /// Read the Salesforce auth tokens
      91            1 : export function getSalesforceDataCloudUrl(endpoint: string | undefined): string {
      92            1 :     if (typeof endpoint !== 'string' || endpoint.trim().length === 0) {
      93            0 :         throw new Error('Data Cloud Instance URL cannot be empty');
      94              :     }
      95            1 :     const value = endpoint.trim();
      96            1 :     const normalized = /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `https://${value}`;
      97            1 :     const url = new URL(normalized);
      98            1 :     if (!/^https:\/\/[^/\\\s@?#]+\/?$/i.test(normalized)
      99            1 :         || url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.pathname !== '/') {
     100            1 :         throw new Error('Data Cloud Instance URL must be an HTTPS origin without credentials, path, query, or fragment');
     101              :     }
     102            1 :     return url.toString();
     103            1 : }
     104              : 
     105            1 : export function collectSalesforceAuthInfo(coreToken: connection.SalesforceCoreAccessToken, offcoreToken: connection.SalesforceDataCloudAccessToken): SalesforceAuthInfo | null {
     106            1 :     const jwt = offcoreToken?.jwt;
     107            1 :     if (jwt) {
     108            1 :         return {
     109            1 :             offcoreRawJwt: jwt.raw,
     110            1 :             offcoreInstanceUrl: offcoreToken?.instanceUrl ?? null,
     111            1 :             offcoreTenantId: jwt.payload?.audienceTenantId ?? null,
     112            1 :             offcoreAccessToken: null,
     113            1 :             coreTenantId: jwt.payload?.audienceTenantId ?? null,
     114            1 :             coreAccessToken: coreToken?.accessToken ?? null,
     115            1 :             dataspace: getSalesforceDataSpace(offcoreToken),
     116            1 :         };
     117              :     } else {
     118            0 :         return null;
     119              :     }
     120            1 : }
     121              : 
     122            1 : function redactTokenDiagnostics(value: unknown): string {
     123            1 :     return JSON.stringify(value, (key, item) => {
     124              :         // Redact credentials and identities at every nesting level, not the routing context.
     125            1 :         const normalized = key.replace(/[-_]/g, '').toLowerCase();
     126            1 :         if ((/(token|secret|password|signature|assertion|authorization|email|username)/.test(normalized)
     127            1 :             && normalized !== 'tokentype' && normalized !== 'tokenformat')
     128            1 :             || ['id', 'sub', 'jti', 'clientid', 'userid', 'orgid', 'sfuid', 'sfoid', 'sfappid',
     129            1 :                 'tnk', 'sfi', 'sfapop', 'code', 'codeverifier', 'name', 'givenname', 'familyname', 'preferredusername'].includes(normalized)) {
     130            1 :             return '<redacted>';
     131              :         }
     132            1 :         return item;
     133            1 :     }) ?? 'null';
     134            1 : }
     135              : 
     136            1 : export function parseCoreAccessToken(obj: any, logger?: Logger): connection.SalesforceCoreAccessToken {
     137            1 :     if (typeof obj?.access_token !== 'string' || !obj.access_token) {
     138            1 :         logger?.warn('Rejected Salesforce OAuth token response', {
     139            1 :             oauthResponse: redactTokenDiagnostics(obj),
     140            1 :         }, LOG_CTX);
     141            1 :         throw new Error('Salesforce OAuth response is missing access_token');
     142            1 :     }
     143              :     // Core OAuth credentials may be opaque; only the exchanged Data Cloud token is decoded.
     144            1 :     return {
     145            1 :         createdAt: new Date().toISOString(),
     146            1 :         accessToken: obj.access_token,
     147            1 :         apiInstanceUrl: obj.api_instance_url,
     148            1 :         id: obj.id,
     149            1 :         idToken: obj.id_token,
     150            1 :         instanceUrl: obj.instance_url,
     151            1 :         issuedAt: obj.issued_at,
     152            1 :         refreshToken: obj.refresh_token,
     153            1 :         scope: obj.scope,
     154            1 :         signature: obj.signature,
     155            1 :         tokenType: obj.token_type,
     156            1 :     };
     157            1 : }
     158              : 
     159            1 : export function parseCoreUserInfo(obj: any): connection.SalesforceCoreUserInfo {
     160            1 :     return ({
     161            1 :         active: obj.active,
     162            1 :         email: obj.email,
     163            1 :         emailVerified: obj.email_verified,
     164            1 :         familyName: obj.family_name,
     165            1 :         givenName: obj.given_name,
     166            1 :         isAppInstalled: obj.is_app_installed,
     167            1 :         isSalesforceIntegrationUser: obj.is_salesforce_integration_user,
     168            1 :         language: obj.language,
     169            1 :         locale: obj.locale,
     170            1 :         name: obj.name,
     171            1 :         nickname: obj.nickname,
     172            1 :         organizationId: obj.organization_id,
     173            1 :         photos: obj.photos,
     174            1 :         picture: obj.picture,
     175            1 :         preferredUsername: obj.preferred_username,
     176            1 :         profile: obj.profile,
     177            1 :         sub: obj.sub,
     178            1 :         updatedAt: obj.updated_at,
     179            1 :         userId: obj.user_id,
     180            1 :         userType: obj.user_type,
     181            1 :         utcOffset: obj.utcOffset,
     182            1 :         zoneinfo: obj.zoneinfo,
     183            1 :     });
     184            1 : }
     185              : 
     186              : export interface SalesforceApiClientInterface {
     187              :     getCoreAccessToken(
     188              :         authConfig: connection.SalesforceOAuthConfig,
     189              :         authParams: connection.SalesforceConnectionParams,
     190              :         authCode: string,
     191              :         pkceVerifier: string,
     192              :         cancel: AbortSignal,
     193              :     ): Promise<connection.SalesforceCoreAccessToken>;
     194              :     getCoreUserInfo(
     195              :         access: connection.SalesforceCoreAccessToken,
     196              :         cancel: AbortSignal,
     197              :     ): Promise<connection.SalesforceCoreUserInfo>;
     198              :     getDataCloudMetadata(
     199              :         access: connection.SalesforceCoreAccessToken,
     200              :         dataSpace: string,
     201              :         cancel: AbortSignal,
     202              :         onProgress?: (progress: SalesforceMetadataProgress) => void,
     203              :     ): Promise<connection.SalesforceDataCloudMetadata>;
     204              :     getDataCloudAccessToken(
     205              :         access: connection.SalesforceCoreAccessToken,
     206              :         cancel: AbortSignal,
     207              :     ): Promise<connection.SalesforceDataCloudAccessToken>;
     208              : }
     209              : 
     210            1 : export class SalesforceApiClient implements SalesforceApiClientInterface {
     211            1 :     logger: Logger;
     212            1 :     httpClient: HttpClient;
     213              : 
     214            1 :     constructor(logger: Logger, httpClient: HttpClient) {
     215            1 :         this.logger = logger;
     216            1 :         this.httpClient = httpClient;
     217            1 :     }
     218              : 
     219            1 :     public async getCoreAccessToken(
     220            1 :         authConfig: connection.SalesforceOAuthConfig,
     221            1 :         authParams: connection.SalesforceConnectionParams,
     222            1 :         authCode: string,
     223            1 :         pkceVerifier: string,
     224            1 :         cancel: AbortSignal,
     225            1 :     ): Promise<connection.SalesforceCoreAccessToken> {
     226            1 :         const params: Record<string, string> = {
     227            1 :             grant_type: 'authorization_code',
     228            1 :             code: authCode!,
     229            1 :             redirect_uri: authConfig.oauthRedirect.toString(),
     230            1 :             client_id: authParams.appConsumerKey,
     231            1 :             code_verifier: pkceVerifier,
     232            1 :             format: 'json',
     233            1 :         };
     234            1 :         if (authParams.appConsumerSecret) {
     235            0 :             params.client_secret = authParams.appConsumerSecret;
     236              :         }
     237            1 :         const body = new URLSearchParams(params);
     238              :         // Get the access token
     239            1 :         const headers = new Headers({
     240            1 :             Accept: 'application/json',
     241            1 :             'Content-Type': 'application/x-www-form-urlencoded',
     242            1 :         });
     243            1 :         const url = new URL(`${authParams.instanceUrl}/services/oauth2/token`);
     244            1 :         const response = await this.httpClient.fetch(url, {
     245            1 :             method: 'POST',
     246            1 :             headers,
     247            1 :             body: body,
     248            1 :             signal: cancel,
     249            1 :         });
     250            1 :         const responseBody = await response.json();
     251            1 :         if (response.status < 200 || response.status >= 300 || responseBody.error) {
     252            1 :             const errorDesc = responseBody.error_description ?? responseBody.error ?? `OAuth token request failed: ${response.status}`;
     253            1 :             this.logger.warn(errorDesc, {
     254            1 :                 endpoint: url.toString(),
     255            1 :                 status: response.status.toString(),
     256            1 :                 oauthResponse: redactTokenDiagnostics(responseBody),
     257            1 :             }, LOG_CTX);
     258            1 :             throw new Error(errorDesc);
     259            1 :         } else {
     260            1 :             const parsed = parseCoreAccessToken(responseBody, this.logger);
     261            1 :             return parsed;
     262              :         }
     263            1 :     }
     264              : 
     265            1 :     protected readDataCloudAccessToken(obj: any): connection.SalesforceDataCloudAccessToken {
     266            1 :         if (typeof obj?.access_token !== 'string' || !obj.access_token) throw new Error('missing access_token');
     267            1 :         if (!obj.instance_url) throw new Error('missing instance_url');
     268            1 :         const jwtParts = obj.access_token.split('.');
     269            1 :         if (jwtParts.length !== 3) throw new Error(`invalid jwt, expected 3 parts, received ${jwtParts.length}`);
     270            1 :         const decoder = new TextDecoder();
     271            1 :         const header = JSON.parse(decoder.decode(BASE64URL_CODEC.decode(jwtParts[0])));
     272            1 :         const payload = JSON.parse(decoder.decode(BASE64URL_CODEC.decode(jwtParts[1])));
     273            1 :         if (!header || typeof header !== 'object' || Array.isArray(header)
     274            1 :             || !payload || typeof payload !== 'object' || Array.isArray(payload)) {
     275            0 :             throw new Error('invalid Data Cloud JWT');
     276              :         }
     277            1 :         const expiresAt = new Date(Number(payload.exp) * 1000);
     278            1 :         if (payload.exp == null || !Number.isFinite(expiresAt.getTime())) throw new Error('invalid Data Cloud JWT expiration');
     279            1 :         return {
     280            1 :             createdAt: new Date().toISOString(),
     281            1 :             tokenType: obj.token_type,
     282            1 :             issuedTokenType: obj.issued_token_type,
     283            1 :             expiresAt: expiresAt.toISOString(),
     284            1 :             jwt: {
     285            1 :                 raw: obj.access_token,
     286            1 :                 header,
     287            1 :                 payload: { ...payload, customAttributes: payload.customAttributes ?? payload.custom_attributes },
     288            1 :             },
     289            1 :             instanceUrl: getSalesforceDataCloudUrl(obj.instance_url),
     290            1 :         };
     291            1 :     }
     292              : 
     293            1 :     public async getDataCloudAccessToken(
     294            1 :         access: connection.SalesforceCoreAccessToken,
     295            1 :         cancel: AbortSignal,
     296            1 :     ): Promise<connection.SalesforceDataCloudAccessToken> {
     297            1 :         const body = new URLSearchParams({
     298            1 :             grant_type: 'urn:salesforce:grant-type:external:cdp',
     299            1 :             subject_token: access.accessToken,
     300            1 :             subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
     301            1 :         });
     302            1 :         const url = new URL(`${access.instanceUrl ?? ''}/services/a360/token`);
     303            1 :         const response = await this.httpClient.fetch(url, {
     304            1 :             method: 'POST',
     305            1 :             headers: new Headers({ Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }),
     306            1 :             body,
     307            1 :             signal: cancel,
     308            1 :         });
     309            1 :         const responseBody = await response.json();
     310            1 :         if (response.status < 200 || response.status >= 300 || responseBody.error) {
     311            1 :             const message = responseBody.error_description ?? responseBody.error ?? `Data Cloud token request failed: ${response.status}`;
     312            1 :             this.logger.warn('Data Cloud token request failed', {
     313            1 :                 endpoint: url.toString(), status: response.status.toString(), oauthResponse: redactTokenDiagnostics(responseBody),
     314            1 :             }, LOG_CTX);
     315            1 :             throw new Error(message);
     316            1 :         }
     317            1 :         try {
     318            1 :             return this.readDataCloudAccessToken(responseBody);
     319            1 :         } catch (error) {
     320            1 :             this.logger.warn('Rejected Data Cloud token response', { oauthResponse: redactTokenDiagnostics(responseBody) }, LOG_CTX);
     321            1 :             throw error;
     322            1 :         }
     323            1 :     }
     324              : 
     325            1 :     public async getCoreUserInfo(
     326            1 :         access: connection.SalesforceCoreAccessToken,
     327            1 :         cancel: AbortSignal,
     328            1 :     ): Promise<connection.SalesforceCoreUserInfo> {
     329            1 :         const params = new URLSearchParams();
     330            1 :         params.set('format', 'json');
     331            1 :         params.set('access_token', access.accessToken ?? '');
     332            1 :         const headers = new Headers({
     333            1 :             authorization: `Bearer ${access.accessToken}`,
     334            1 :             accept: 'application/json',
     335            1 :         });
     336            1 :         const url = new URL(`${access.instanceUrl ?? ""}/services/oauth2/userinfo?${params.toString()}`);
     337            1 :         const response = await this.httpClient.fetch(url, {
     338            1 :             headers,
     339            1 :             signal: cancel,
     340            1 :         });
     341            1 :         const responseJson = await response.json();
     342            1 :         return parseCoreUserInfo(responseJson);
     343            1 :     }
     344              : 
     345            1 :     public async getDataCloudMetadata(
     346            1 :         access: connection.SalesforceCoreAccessToken,
     347            1 :         dataSpace: string,
     348            1 :         cancel: AbortSignal,
     349            1 :         onProgress: (progress: SalesforceMetadataProgress) => void = () => { },
     350            1 :     ): Promise<connection.SalesforceDataCloudMetadata> {
     351            1 :         const base = (access.instanceUrl ?? "").replace(/\/+$/, '');
     352            1 :         const headers = new Headers({
     353            1 :             authorization: `Bearer ${access.accessToken}`,
     354            1 :             accept: 'application/json',
     355            1 :         });
     356            1 :         const endpoint = `${base}/services/data/${SALESFORCE_API_VERSION}/ssot/metadata`;
     357            1 :         this.logger.info("Requesting Salesforce metadata", {
     358            1 :             method: "GET",
     359            1 :             endpoint,
     360            1 :             apiVersion: SALESFORCE_API_VERSION,
     361            1 :             dataSpace,
     362            1 :             entityTypes: SALESFORCE_METADATA_ENTITY_TYPES.join(','),
     363            1 :         }, LOG_CTX);
     364            1 :         const requestStartedAt = performance.now();
     365            1 :         const requestResults = await Promise.allSettled(SALESFORCE_METADATA_ENTITY_TYPES.map(async entityType => {
     366            1 :             const url = new URL(endpoint);
     367            1 :             url.searchParams.set('dataspace', dataSpace);
     368            1 :             url.searchParams.set('entityType', entityType);
     369            1 :             onProgress({ collection: entityType, page: 1, loaded: 0, total: null, state: 'requesting' });
     370            1 :             try {
     371            1 :                 const response = await this.httpClient.fetch(url, {
     372            1 :                     headers,
     373            1 :                     signal: cancel,
     374            1 :                 });
     375            1 :                 if (response.status < 200 || response.status >= 300) {
     376            1 :                     const bodyText = await response.text().catch(() => '');
     377            1 :                     throw new Error(`${response.status} ${response.statusText}${bodyText ? ` — ${bodyText.slice(0, 200)}` : ''}`);
     378            1 :                 }
     379            1 :                 const body = await response.json();
     380            1 :                 const entities = Array.isArray(body.metadata) ? body.metadata : [];
     381            1 :                 onProgress({
     382            1 :                     collection: entityType,
     383            1 :                     page: 1,
     384            1 :                     loaded: entities.length,
     385            1 :                     total: entities.length,
     386            1 :                     state: 'complete',
     387            1 :                 });
     388            1 :                 return entities;
     389            1 :             } catch (error) {
     390            1 :                 const message = error instanceof Error ? error.message : String(error);
     391            1 :                 this.logger.warn('Salesforce metadata request failed', {
     392            1 :                     dataSpace,
     393            1 :                     entityType,
     394            1 :                     error: message,
     395            1 :                 }, LOG_CTX);
     396            1 :                 onProgress({
     397            1 :                     collection: entityType,
     398            1 :                     page: 1,
     399            1 :                     loaded: 0,
     400            1 :                     total: null,
     401            1 :                     state: 'failed',
     402            1 :                     error: message,
     403            1 :                 });
     404            1 :                 throw new Error(`Salesforce ${entityType} metadata request failed: ${message}`);
     405            1 :             }
     406            1 :         }));
     407            1 :         const progressByCollection = new Map<string, SalesforceMetadataProgress>();
     408            1 :         const responseEntities: any[][] = [];
     409            1 :         let failedRequests = 0;
     410            1 :         requestResults.forEach((result, index) => {
     411            1 :             const collection = SALESFORCE_METADATA_ENTITY_TYPES[index];
     412            1 :             if (result.status === 'fulfilled') {
     413            1 :                 responseEntities.push(result.value);
     414            1 :                 progressByCollection.set(collection, {
     415            1 :                     collection,
     416            1 :                     page: 1,
     417            1 :                     loaded: result.value.length,
     418            1 :                     total: result.value.length,
     419            1 :                     state: 'complete',
     420            1 :                 });
     421            1 :             } else {
     422            1 :                 const message = result.reason instanceof Error ? result.reason.message : String(result.reason);
     423            1 :                 failedRequests += 1;
     424            1 :                 progressByCollection.set(collection, {
     425            1 :                     collection,
     426            1 :                     page: 1,
     427            1 :                     loaded: 0,
     428            1 :                     total: null,
     429            1 :                     state: 'failed',
     430            1 :                     error: message,
     431            1 :                 });
     432            1 :             }
     433            1 :         });
     434            1 :         if (failedRequests === SALESFORCE_METADATA_ENTITY_TYPES.length) {
     435            1 :             throw new Error(formatSalesforceMetadataProgress(progressByCollection));
     436              :         }
     437            1 :         this.logger.info("Received Salesforce metadata response", {
     438            1 :             durationMs: (performance.now() - requestStartedAt).toFixed(2),
     439            1 :             dataSpace,
     440            1 :             failedRequests: failedRequests.toString(),
     441            1 :         }, LOG_CTX);
     442              : 
     443              :         // Parse the Data Cloud metadata
     444            1 :         const entities: connection.SalesforceDataCloudMetadataEntity[] = [];
     445            1 :         let fieldCount = 0;
     446            1 :         for (const collectionEntities of responseEntities) {
     447            1 :             for (const entityJson of collectionEntities) {
     448            1 :                 const rawFields = entityJson.fields ?? [];
     449            1 :                 const fields: connection.SalesforceDataCloudMetadataEntityField[] = [];
     450            1 :                 if (Array.isArray(rawFields)) {
     451            1 :                     for (const fieldJson of rawFields) {
     452            1 :                         fields.push(({
     453            1 :                             name: fieldJson.name ?? '',
     454            1 :                             displayName: fieldJson.displayName ?? '',
     455            1 :                             type: fieldJson.type ?? '',
     456            1 :                             businessType: fieldJson.businessType ?? '',
     457            1 :                         }));
     458            1 :                         fieldCount += 1;
     459            1 :                     }
     460              :                 }
     461            1 :                 const primaryKeys: connection.SalesforceDataCloudMetadataPrimaryKey[] = [];
     462            1 :                 if (entityJson.primaryKeys && Array.isArray(entityJson.primaryKeys)) {
     463            1 :                     for (const pkJson of entityJson.primaryKeys) {
     464            1 :                         primaryKeys.push(({
     465            1 :                             indexOrder: pkJson.indexOrder ?? '',
     466            1 :                             name: pkJson.name ?? '',
     467            1 :                             displayName: pkJson.displayName ?? '',
     468            1 :                         }));
     469              :                     }
     470              :                 }
     471            1 :                 entities.push(({
     472            1 :                     name: entityJson.name ?? '',
     473            1 :                     displayName: entityJson.displayName ?? '',
     474            1 :                     category: entityJson.category ?? '',
     475            1 :                     fields: fields,
     476            1 :                     primaryKeys: primaryKeys,
     477            1 :                 }));
     478            1 :             }
     479              :         }
     480            1 :         this.logger.info("Parsed Salesforce metadata", {
     481            1 :             dataSpace,
     482            1 :             entities: entities.length.toString(),
     483            1 :             fields: fieldCount.toString(),
     484            1 :         }, LOG_CTX);
     485            1 :         return ({
     486            1 :             metadata: entities
     487            1 :         });
     488            1 :     }
     489            1 : }
     490              : 
     491            1 : export class SalesforceDatabaseChannel implements HyperDatabaseChannel {
     492              :     /// The api client
     493            1 :     protected apiClient: SalesforceApiClientInterface;
     494              :     /// The core access token
     495            1 :     public readonly coreToken: connection.SalesforceCoreAccessToken;
     496            1 :     public readonly dataCloudToken: connection.SalesforceDataCloudAccessToken;
     497              :     /// The Hyper database channel
     498            1 :     hyperChannel: HyperDatabaseChannel;
     499              : 
     500              :     /// The constructor
     501            1 :     constructor(apiClient: SalesforceApiClientInterface, coreToken: connection.SalesforceCoreAccessToken, dataCloudToken: connection.SalesforceDataCloudAccessToken, channel: HyperDatabaseChannel) {
     502            1 :         this.apiClient = apiClient;
     503            1 :         this.coreToken = coreToken;
     504            1 :         this.dataCloudToken = dataCloudToken;
     505            1 :         this.hyperChannel = channel;
     506            1 :     }
     507              : 
     508              :     /// Execute Query
     509            0 :     async executeQuery(param: pb.salesforce_hyperdb_grpc_v1.pb.QueryParam, abort?: AbortSignal, options?: HyperQueryExecutionOptions): Promise<HyperQueryResultStream> {
     510            0 :         return this.hyperChannel.executeQuery(param, abort, options);
     511            0 :     }
     512              :     /// Destroy the connection
     513            0 :     async close(): Promise<void> {
     514            0 :         return this.hyperChannel.close();
     515            0 :     }
     516            1 : }
        

Generated by: LCOV version 2.0-1