Created
January 20, 2023 21:45
-
-
Save mpurdon/4be05b19a6363d8edddb4fbd477fc1d0 to your computer and use it in GitHub Desktop.
An example of functional command pattern in TS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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); | |
} | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage: