HammerTech Example
The code below is an example of a process connecting to HammerTech. 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. It pages through the Projects endpoint.
INFO
This process requires HammerTech credentials (email, password and tenant), stored as Secrets 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 (e.g. api/hammertech.js) and load it with script.library.
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.');
}