Skip to content

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.');
}