---
url: /technical/process-hammertech-example.md
---

# HammerTech Example

The code below is an example of a process connecting to [HammerTech](https://hammertechonline.com/). HammerTech authenticates with an email, password and tenant, generating its own token rather than using MODLR's built-in OAuth 2.0 flow, so this is an [Advanced Integration](/technical/integrations.html#advanced-integrations). It pages through the `Projects` endpoint.

::: info
This process requires HammerTech credentials (email, password and tenant), stored as [Secrets](/process-functions/security-getsecret.html) rather than hardcoded in the process.
:::

## Library Code

A small client for the HammerTech API, handling authentication and wrapping `GET`/`POST` requests. It also includes a helper for building OData-style advanced filter queries. Save this as a [Process Library](/technical/process-libraries.html) (e.g. `api/hammertech.js`) and load it with [`script.library`](/process-functions/script-library.html).

```js
class HammerTech {

    constructor(email, password, tenant, region = "au") {
        this.email = email;
        this.password = password;
        this.tenant = tenant;
        this.region = region;
        this.token = null;
        this.authUrl = `https://${this.region}-auth.hammertechonline.com/api/login/generatetoken`;
        this.baseUrl = `https://${this.region}-api.hammertechonline.com/api/v1/`;
        this.debug = false;
    }

    enableDebug(debug) {
        this.debug = debug;
    }

    generateToken() {
        const { token } = this.post(this.authUrl, {
            email: this.email,
            password: this.password,
            tenant: this.tenant
        });

        this.token = token;
        return token;
    }

    request(method, path, headers = {}, body = {}) {
        var fullUrl = this.buildUrl(path);

        const requestHeaders = {
            "Accept": "application/json",
            "Content-Type": "application/json",
            ...headers,
            ...(this.token ? { "Authorization": `Bearer ${this.token}` } : {})
        };

        fullUrl = cleanQueryString(fullUrl);

        const response = web.request(method, fullUrl, requestHeaders, body, false);
        if (this.debug) {
            console.log(`[DEBUG] ${fullUrl}, Response: ${response.status}`);
        }

        if (response.status >= 200 && response.status < 300) {
            return JSON.parse(response.body);
        } else if (response.status === 429) {
            throw new Error("Rate limited by HammerTech (HTTP 429).");
        } else {
            console.log("Request failed:", fullUrl, response);
        }
    }

    get(path, parameters = {}, headers = {}) {
        const queryString = this.buildQueryString(parameters);
        return this.request("GET", path + queryString, headers);
    }

    post(path, data, headers = {}) {
        return this.request("POST", path, headers, data);
    }

    buildQueryString(parameters) {
        if (Object.keys(parameters).length === 0) {
            return "";
        }

        return "?" + Object.entries(parameters)
            .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
            .join('&');
    }

    buildUrl(path) {
        if (path.startsWith("http")) {
            return path;
        }

        const trimmedPath = path.startsWith("/") ? path.substring(1) : path;
        return `${this.baseUrl}${trimmedPath}`;
    }

    // Builds an OData-style $filter query string, e.g. { updatedAt: { operator: ">", operand: "2024-01-01" } }
    buildAdvancedQueryString(parameters) {
        if (Object.keys(parameters).length === 0) {
            return "";
        }

        const odataFilters = [];
        const otherParams = [];

        Object.entries(parameters).forEach(([key, value]) => {
            if (Array.isArray(value)) {
                // Handle multiple conditions for the same key
                value.forEach(condition => {
                    if (typeof condition === "object" && condition !== null) {
                        const { operator, operand } = condition;
                        const odataOperator = this.getODataOperator(operator);
                        if (odataOperator) {
                            odataFilters.push(`${key} ${odataOperator} ${operand}`);
                        } else {
                            throw new Error(`Unsupported operator: ${operator}`);
                        }
                    } else {
                        throw new Error("Array elements must be objects with operator and operand.");
                    }
                });
            } else if (typeof value === "object" && value !== null) {
                // Single condition for the key
                const { operator, operand } = value;
                const odataOperator = this.getODataOperator(operator);
                if (odataOperator) {
                    odataFilters.push(`${key} ${odataOperator} ${operand}`);
                } else {
                    throw new Error(`Unsupported operator: ${operator}`);
                }
            } else {
                // Add other query parameters (non-filter parameters)
                otherParams.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
            }
        });

        const filterString = odataFilters.length > 0 ? `$filter=${encodeURIComponent(odataFilters.join(' and '))}` : "";
        const queryParams = [filterString, ...otherParams].filter(Boolean).join('&');

        return queryParams ? `?${queryParams}` : "";
    }

    getWithAdvancedQuery(path, parameters = {}, headers = {}) {
        const queryString = this.buildAdvancedQueryString(parameters);
        return this.request("GET", path + queryString, headers);
    }

    getODataOperator(operator) {
        const operatorMap = {
            ">": "gt",
            ">=": "ge",
            "<": "lt",
            "<=": "le",
            "=": "eq"
        };
        return operatorMap[operator];
    }
}

function cleanQueryString(url) {
    const parts = url.split("?");
    if (parts.length > 2) {
        // Rebuild the URL, keeping only the first '?' and merging the remaining parts with '&'
        return parts[0] + "?" + parts.slice(1).join("&");
    }
    return url;
}
```

## Process Code

This example pages through the `Projects` endpoint, 100 results at a time.

```js
script.library("api/hammertech.js");

let endpoint = "Projects";
let parameters = {
    skip: 0,
    includeArchived: true
};

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 email = security.getSecret("HAMMERTECH_EMAIL");
    const password = security.getSecret("HAMMERTECH_PASSWORD");
    const tenant = security.getSecret("HAMMERTECH_TENANT");

    const api = new HammerTech(email, password, tenant, "au");

    try {
        api.generateToken();
    } catch (e) {
        console.log("Could not login");
        return;
    }

    let results = api.get(endpoint, parameters);
    if (!results) {
        return;
    }

    console.log(`Fetched ${results.length} results.`);
    console.log(results);

    while (results.length > 99) {
        parameters.skip += 100;
        results = api.get(endpoint, parameters);
        if (!results) {
            return;
        }

        console.log(`Fetched ${results.length} results.`);
        console.log(results);
    }
}

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