Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: salesforce sdk pattern #3946

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { HttpClient } from "../utils/httpClient";
import { SalesforceAuth, AuthProvider, SalesforceRecord, SalesforceResponse, QueryResponse, OAuthCredentials, SalesforceDestinationConfig } from "../types/salesforceTypes";
import { TokenProvider } from "../auth/tokenProvider";
import { OAuthProvider } from "../auth/oauthProvider";

export class SalesforceClient {
private httpClient: HttpClient;

constructor(auth: SalesforceAuth) {
let authProvider: AuthProvider;
let instanceUrl: string;

if ("accessToken" in auth) {
authProvider = new OAuthProvider(auth as OAuthCredentials );
instanceUrl = auth.instanceUrl;
} else {
authProvider = new TokenProvider(auth as SalesforceDestinationConfig);
instanceUrl = auth.instanceUrl;
}

this.httpClient = new HttpClient(instanceUrl, authProvider);
}

async create(objectType: string, record: SalesforceRecord, salesforceId?: string): Promise<SalesforceResponse> {
let targetEndpoint = `/services/data/v50.0/sobjects/${objectType}`;
if (salesforceId) {
targetEndpoint += `/${salesforceId}?_HttpMethod=PATCH`;
}
return this.httpClient.post<SalesforceResponse>(targetEndpoint, record);
}

async search(objectType: string, identifierValue: string, identifierType: string): Promise<QueryResponse<SalesforceRecord>> {
return this.httpClient.get<QueryResponse<SalesforceRecord>>(`/services/data/v50.0/parameterizedSearch/?q=${identifierValue}&sobject=${objectType}&in=${identifierType}&${objectType}.fields=id,${identifierType}`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* AuthProvider interface acts as a contract for authentication mechanisms.
* Each implementation of this interface will define how to retrieve the access token.
*/
export interface AuthProvider {
/**
* Retrieves the access token required for authenticating API calls.
* @returns A Promise that resolves to the access token as a string.
*/
getAccessToken(): Promise<string>;

getAuthenticationHeader(token: string): any;

areCredentialsSet(): boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { AuthProvider } from './authContext';
import { OAuthCredentials } from '../types/salesforceTypes';

/**
* OAuthProvider is an implementation of AuthProvider that retrieves an access token using OAuth.
*/
export class OAuthProvider implements AuthProvider {
private readonly credentials: OAuthCredentials;

constructor(credentials: OAuthCredentials) {
if (!credentials.token || !credentials.instanceUrl) {
throw new Error('OAuth credentials are incomplete.');
}
this.credentials = credentials;
}

async getAccessToken(): Promise<string> {
return this.credentials.token;
}

getAuthenticationHeader(token: string): any {
return {
Authorization: `Bearer ${token}`,
};
}

areCredentialsSet(): boolean {
return !!(this.credentials && this.credentials.token && this.credentials.instanceUrl);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import axios from 'axios';
import { AuthProvider } from './authContext';
import {
SalesforceDestinationConfig,
SF_TOKEN_REQUEST_URL,
SF_TOKEN_REQUEST_URL_SANDBOX,
} from '../types/salesforceTypes';

/**
* TokenProvider is an implementation of AuthProvider that uses a pre-existing access token.
*/
export class TokenProvider implements AuthProvider {
private credentials!: SalesforceDestinationConfig;

// Setter method for credentials to validate before setting
setCredentials(credentials: SalesforceDestinationConfig): void {
if (!credentials.consumerKey || !credentials.password || !credentials.consumerSecret) {
throw new Error('Access token is required for TokenProvider.');
}
this.credentials = credentials;
}

constructor(credentials: SalesforceDestinationConfig) {
this.setCredentials(credentials); // Use the setter method
}

async getAccessToken(): Promise<string> {
let SF_TOKEN_URL;
if (this.credentials.sandbox) {
SF_TOKEN_URL = SF_TOKEN_REQUEST_URL_SANDBOX;
} else {
SF_TOKEN_URL = SF_TOKEN_REQUEST_URL;
}

try {
const authUrl = `${SF_TOKEN_URL}?username=${
this.credentials.userName
}&password=${encodeURIComponent(this.credentials.password)}${encodeURIComponent(
this.credentials.initialAccessToken,
)}&client_id=${this.credentials.consumerKey}&client_secret=${
this.credentials.consumerSecret
}&grant_type=password`;
const response = await axios.post(authUrl);

if (response.data && response.data.access_token) {
this.credentials.instanceUrl = response.data.instance_url;
return response.data.access_token;
}
throw new Error('Failed to retrieve access token.');
} catch (error: unknown) {
if (error instanceof Error) {
throw new Error(`Error fetching access token: ${error.message}`);
} else {
throw new Error('Error fetching access token: Unknown error occurred.');
}
}
}

getAuthenticationHeader(token: string): any {
return {
Authorization: token,
};
}

areCredentialsSet(): boolean {
return !!(
this.credentials &&
this.credentials.consumerKey &&
this.credentials.password &&
this.credentials.consumerSecret &&
this.credentials.instanceUrl
);
}
}
22 changes: 22 additions & 0 deletions src/v0/destinations/salesforce/salesforce-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { AuthProvider } from './auth/authContext';
import { TokenProvider } from './auth/tokenProvider';
import { OAuthProvider } from './auth/oauthProvider';
import {
SalesforceDestinationConfig,
OAuthCredentials,
LEGACY,
OAUTH,
} from './types/salesforceTypes';

export function createAuthProvider(
authType: 'legacy' | 'oauth',
metadata: SalesforceDestinationConfig | OAuthCredentials,
): AuthProvider {
if (authType === LEGACY) {
return new TokenProvider(metadata as SalesforceDestinationConfig);
}
if (authType === OAUTH) {
return new OAuthProvider(metadata as OAuthCredentials);
}
throw new Error(`Unsupported auth type: ${authType}`);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export interface OAuthCredentials {
token: string;
instanceUrl: string;
}

export interface SalesforceDestinationConfig {
initialAccessToken: string;
consumerKey: string;
consumerSecret: string;
userName: string;
password: string;
sandbox: true;
instanceUrl: string;
}

export type SalesforceAuth = SalesforceDestinationConfig | OAuthCredentials;

export interface AuthProvider {
getAccessToken(): Promise<string>;
getAuthenticationHeader(token: string): any;
}

export interface SalesforceResponse {
id: string;
success: boolean;
errors?: string[];
}

export interface SalesforceRecord {
Id: string;
Name: string;
}

export interface QueryResponse<T> {
totalSize: number;
done: boolean;
records: T[];
}

export const SF_TOKEN_REQUEST_URL = 'https://login.salesforce.com/services/oauth2/token';
export const SF_TOKEN_REQUEST_URL_SANDBOX = 'https://test.salesforce.com/services/oauth2/token';

export const LEGACY = 'legacy';
export const OAUTH = 'oauth';
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import axios, { AxiosInstance } from 'axios';
import { AuthProvider } from '../types/salesforceTypes';

export class HttpClient {
private client: AxiosInstance;

private authProvider: AuthProvider;

constructor(instanceUrl: string, authProvider: AuthProvider) {
this.authProvider = authProvider;
this.client = axios.create({
baseURL: instanceUrl,
headers: {
'Content-Type': 'application/json',
},
});
}

private async addAuthHeader(): Promise<void> {
const token = await this.authProvider.getAccessToken();
this.client.defaults.headers = this.authProvider.getAuthenticationHeader(token);
}

async get<T>(url: string): Promise<T> {
// for getting the access token we are not requiring to send any headers
const response = await this.client.get<T>(url);
return response.data;
}

async post<T>(url: string, data: any): Promise<T> {
await this.addAuthHeader();
const response = await this.client.post<T>(url, data);
return response.data;
}
}
Loading
Loading