Skip to content

Instantly share code, notes, and snippets.

@mpurdon
Created January 20, 2023 21:45
Show Gist options
  • Save mpurdon/4be05b19a6363d8edddb4fbd477fc1d0 to your computer and use it in GitHub Desktop.
Save mpurdon/4be05b19a6363d8edddb4fbd477fc1d0 to your computer and use it in GitHub Desktop.
An example of functional command pattern in TS
/**
* Zoho API Client
*
* Uses the Functional Options pattern to allow for a cleaner constructor
*
* @see: https://medium.com/swlh/using-a-golang-pattern-to-write-better-typescript-58044b56b26c
*/
import axios from 'axios';
import {Logger} from '@aws-lambda-powertools/logger';
import {Tracer} from '@aws-lambda-powertools/tracer';
import {Config} from '@serverless-stack/node/config';
/**
* Represents a token from Zoho
*/
type ZohoToken = {
refreshToken: string;
accessToken: string;
apiDomain: string;
};
/**
* Represents a contact from Zoho CRM
*/
type ZohoContact = {
id: string;
email: string;
mobile: string;
firstName: string;
lastName: string;
};
/**
* Options for the Zoho Client
*/
type ClientOption = (c: Client) => void;
/**
* Zoho API Client
*/
export class Client {
private config: typeof Config;
private logger: Logger;
private tracer: Tracer;
/**
* Constructor
*
* This uses the Functional Options Pattern
*/
constructor(...options: ClientOption[]) {
// set any defaults
// set the options
for (const option of options) {
option(this);
}
}
/**
* Add a Config
*/
public static WithConfig(config: typeof Config): ClientOption {
return (c: Client): void => {
c.config = config;
};
}
/**
* Add a logger
*/
public static WithLogger(logger: Logger): ClientOption {
return (c: Client): void => {
c.logger = logger;
};
}
/**
* Add a tracer
*/
public static WithTracer(tracer: Tracer): ClientOption {
return (c: Client): void => {
c.tracer = tracer;
};
}
/**
* Get a contact from Zoho by phone number
*/
public getContact = async (
clientPhoneNumber: string,
): Promise<ZohoContact> => {
const handlerSegment = this.tracer.getSegment();
const subsegment = handlerSegment.addNewSubsegment('getContact');
this.tracer.setSegment(subsegment);
/**
* Factory method for a ZohoContact from the API response
* @param data
*/
const contactFromAPI = (data): ZohoContact => {
return {
id: data.id,
email: data.Email,
mobile: data.Mobile,
firstName: data.First_Name,
lastName: data.Last_Name,
};
};
let contact = null;
try {
const token: ZohoToken = await this.getToken();
this.logger.debug({
message: `getting contact for ${clientPhoneNumber}`,
});
const queryData = {
criteria: `(Mobile:equals:${clientPhoneNumber.replace('+', '')})`, // No leading +
};
const url = `${token.apiDomain}/crm/v2/contacts/search`;
const response = await axios.get(url, {
params: queryData,
headers: {
Authorization: `Bearer ${token.accessToken}`,
},
});
this.logger.debug({
message: `response from Zoho for Contact search ${url}`,
queryData,
status: response.status,
statusText: response.statusText,
});
if (response.status === 204) {
this.logger.error('no contact could be found for the given 10DLC');
return contact;
}
contact = contactFromAPI(response.data.data[0]);
} catch (e) {
this.logger.error('could not get contact from Zoho', e as Error);
} finally {
subsegment.close();
this.tracer.setSegment(handlerSegment);
}
return contact;
};
/**
* Get a user from Zoho by phone number
*/
public getUser = async (
userPhoneNumber: string,
): Promise<Record<string, any>> => {
const handlerSegment = this.tracer.getSegment();
const subsegment = handlerSegment.addNewSubsegment('getUser');
this.tracer.setSegment(subsegment);
let contact = null;
try {
const token: ZohoToken = await this.getToken();
this.logger.debug({
message: `getting user for ${userPhoneNumber}`,
});
const queryData = {
criteria: `(Mobile:equals:${userPhoneNumber.replace('+', '')})`, // No leading +
};
const url = `${token.apiDomain}/crm/v2/users/search`;
const response = await axios.get(url, {
params: queryData,
headers: {
Authorization: `Bearer ${token.accessToken}`,
},
});
this.logger.debug({
message: `response from Zoho for user search ${url}`,
queryData,
status: response.status,
statusText: response.statusText,
});
if (response.status === 204) {
this.logger.error('no user could be found for the given 10DLC');
return contact;
}
contact = response.data.data[0];
} catch (e) {
this.logger.error('could not get user from Zoho', e as Error);
} finally {
subsegment.close();
this.tracer.setSegment(handlerSegment);
}
return contact;
};
/**
* Get a Token suitable for connecting to Zoho's API
*
* Example response (not real don't freak out):
*
* {
* "data": {
* "refresh_token": "1000.1211a3cc7491a8766411e9acca5ac58f.1bb59f2e8c215cc3ee9485baf27934fc",
* "access_token": "1000.075a29ebe1756d8d9fd0add91bffebcc.4841f63f5634647f921e91cf2f85b849",
* "api_domain": "https://www.zohoapis.com",
* "token_type": "Bearer",
* "expires_in": 3600,
* "issuedAt": 1671234460
* },
* "errors": []
* }
*/
protected getToken = async (): Promise<ZohoToken> => {
const handlerSegment = this.tracer.getSegment();
const subsegment = handlerSegment.addNewSubsegment('getToken');
this.tracer.setSegment(subsegment);
try {
this.logger.debug({
message: 'getting Zoho token from Token services',
url: this.config.CRM_TOKEN_SERVICE_URL,
});
const response = await axios.get(this.config.CRM_TOKEN_SERVICE_URL);
this.logger.debug({
message: 'response from Token services',
status: response.status,
statusText: response.statusText,
});
const data = response.data.data;
return {
refreshToken: data.refresh_token,
accessToken: data.access_token,
apiDomain: data.api_domain,
};
} catch (e) {
this.logger.error('could not get a Zoho Token', e as Error);
throw e;
} finally {
subsegment.close();
this.tracer.setSegment(handlerSegment);
}
};
}
@mpurdon
Copy link
Author

mpurdon commented Jan 20, 2023

Usage:

import { Client as ZohoClient } from './zoho';

// Create the Zoho Client
let zohoClient = new ZohoClient(
  ZohoClient.WithConfig(Config),
  ZohoClient.WithTracer(tracer),
  ZohoClient.WithLogger(logger),
);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment