Eagle CRM Example
The code below is an example of a process connecting to Eagle CRM's GraphQL API. Eagle CRM authenticates with a client ID and secret exchanged for a bearer token, rather than MODLR's built-in OAuth 2.0 flow, so this is an Advanced Integration. It pages through the properties query using a cursor.
INFO
This process requires an Eagle CRM Client ID and Client Secret, stored as Secrets rather than hardcoded in the process.
Library Code
A client for the Eagle CRM API, handling token retrieval/expiry and wrapping GraphQL queries. getProperties/getPropertiesIterator below is one example query - the same query() method can be used to add further queries for other Eagle CRM resources (e.g. contracts, agents, notes). Save this as a Process Library and load it with script.library.
js
class EagleApi {
/**
* @param {string} clientId
* @param {string} clientSecret
* @param {string} [baseUrl="https://www.eagleagent.com.au/api/v3"] - The base URL of the API.
*/
constructor(clientId, clientSecret, baseUrl = "https://www.eagleagent.com.au/api/v3") {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.baseUrl = baseUrl;
this.token = null;
this.tokenExpiry = null;
}
/**
* Retrieves and sets the access token for the API.
* @private
*/
_getToken() {
const url = `${this.baseUrl}/token`;
const headers = {
"Authorization": `Bearer ${this.clientId}:${this.clientSecret}`,
"Content-Type": "application/json"
};
const response = web.request('POST', url, headers, null, false);
if (response.status >= 200 && response.status < 300) {
const data = JSON.parse(response.body);
if (data && data.data && data.data.token && data.data.token.token && data.data.token.expiresAt) {
this.token = data.data.token.token;
this.tokenExpiry = data.data.token.expiresAt;
} else {
throw new Error("Malformed token response.");
}
} else {
console.log("Failed to obtain access token. Response body:", response.body);
throw new Error(`Token request failed with status code ${response.status}`);
}
}
/**
* Checks if the current token is expired or near expiry. (They last 24 hours)
* @private
*/
_isTokenExpired() {
if (!this.token || !this.tokenExpiry) return true;
const now = Math.floor(Date.now() / 1000);
return now >= this.tokenExpiry - (60 * 5); // Refresh 5 minutes before expiry
}
/**
* Makes an HTTP request to the Eagle CRM API.
* @private
*/
_fetch(url, method = 'GET', body = null, retryAttempt = 0) {
if (this._isTokenExpired()) {
this._getToken();
}
const absoluteUrl = `${this.baseUrl}/${url}`;
const headers = {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json"
};
const response = web.request(method, absoluteUrl, headers, body ? JSON.stringify(body) : null, false);
if (response.status === 401 && retryAttempt === 0) {
console.log("Possible access token expired, attempting to refresh...");
this._getToken();
return this._fetch(url, method, body, retryAttempt + 1);
}
if (response.status === 500 && retryAttempt < 3) {
console.log(`Server error (500), retrying after 1 second... (attempt ${retryAttempt + 1}/3)`);
script.sleep(1000);
return this._fetch(url, method, body, retryAttempt + 1);
}
if (response.status >= 200 && response.status < 300) {
return JSON.parse(response.body);
} else {
console.log(`Request failed. Status ${response.status}. Body:`, response.body);
throw new Error(`Request failed with status ${response.status}`);
}
}
/**
* Executes a GraphQL query against the API.
*/
query(graphqlQuery, variables = {}) {
return this._fetch("graphql", "POST", { query: graphqlQuery, variables });
}
/**
* Fetches properties with pagination support.
*/
getProperties(pageSize = 20, cursor = null) {
const query = `
query GetAllProperties($first: Int, $after: String) {
properties(first: $first, after: $after) {
nodes {
id
formattedAddress
price
status
createdAt
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}
`;
const variables = { first: pageSize };
if (cursor) variables.after = cursor;
return this.query(query, variables);
}
/**
* Creates a properties iterator for memory-efficient processing of large datasets.
*/
*getPropertiesIterator(pageSize = 20) {
let hasNextPage = true;
let cursor = null;
while (hasNextPage) {
const response = this.getProperties(pageSize, cursor);
if (response.data && response.data.properties) {
const { nodes, pageInfo } = response.data.properties;
hasNextPage = pageInfo.hasNextPage;
cursor = pageInfo.endCursor;
yield {
properties: nodes,
pageInfo: pageInfo,
totalCount: response.data.properties.totalCount || nodes.length
};
script.sleep(100);
} else {
throw new Error("Invalid response structure from properties query");
}
}
}
}Process Code
js
script.library("libraries/eagle.js");
function pre() {
// This function is called once before the process is executed.
// Use this to setup prompts.
script.log('process pre-execution parameters parsed.');
}
function begin() {
// This function is called once at the start of the process
script.log('process execution started.');
const api = new EagleApi(security.getSecret("EAGLE_CLIENT_ID"), security.getSecret("EAGLE_CLIENT_SECRET"));
for (const page of api.getPropertiesIterator(50)) {
if (script.IsCancelled()) {
return;
}
console.log(`Fetched ${page.properties.length} of ${page.totalCount} properties.`);
console.log(page.properties);
}
}
function data(record) {
// This function is called once for each line of data on the second cycle
// Use this to build dimensions and push data into cubes
}
function end() {
// This function is called once at the end of the process
script.log('process execution finished.');
}