PropertyMe Example
The code below is an example of a process connecting to the PropertyMe API. PropertyMe uses its own OAuth 2.0 refresh token flow rather than MODLR's built-in OAuth 2.0 flow, so this is an Advanced Integration.
INFO
This process requires a PropertyMe Client ID and Client Secret, stored as the Model Variables property-me-client-id and property-me-client-secret. It also expects a row already present in a propertyme.connections table (matched by connectionName) holding a valid access/refresh token pair - this class only reads and refreshes that token, it doesn't perform the initial authorization.
Library Code
A client for the PropertyMe API. It loads the current access/refresh token from the datastore, automatically refreshes it (and persists the refreshed token) on a 401, and retries once on a 500. Save this as a Process Library and load it with script.library.
js
// https://app.propertyme.com/api/swagger-ui/
class PropertyMeApi {
/**
* @param {string} connectionName
* @param {string} [baseUrl="https://app.propertyme.com/api/v1/"] - The base URL of the API.
*/
constructor(connectionName, baseUrl = "https://app.propertyme.com/api/v1/") {
this.connectionName = connectionName;
this.baseUrl = baseUrl;
this.accessToken = null;
this.refreshToken = null;
this.expiresIn = null;
this.CLIENT_ID = script.variableGet("property-me-client-id");
this.CLIENT_SECRET = script.variableGet("property-me-client-secret");
this._getTokens();
}
/**
* Retrieves and sets the access token for the API.
* @private
* @throws Will throw an error if the token cannot be obtained.
*/
_getTokens() {
const query = `SELECT * FROM propertyme.connections WHERE name = ? AND state IS NULL`;
const connections = JSON.parse(datasource.select("Internal Datastore", query, [this.connectionName]));
const connection = connections && connections[0];
if (!connection) {
throw new Error(`Invalid connection name: '${this.connectionName}'`);
}
if (!connection.response) {
throw new Error(`No tokens found with '${this.connectionName}', try renewing`);
}
const tokens = JSON.parse(connection.response);
this.expiresIn = tokens.expires_in;
this.accessToken = tokens.access_token;
this.refreshToken = tokens.refresh_token;
return tokens;
}
/**
* Saves updated tokens back to the datastore.
* @private
* @param {object} tokens - The token object to save.
*/
_saveTokens(tokens) {
const updateQuery = `UPDATE propertyme.connections SET response = ? WHERE name = ? AND state IS NULL`;
datasource.update("Internal Datastore", updateQuery, [JSON.stringify(tokens), this.connectionName]);
}
/**
* Marks the connection as bad, so it's picked up for renewal.
* @private
*/
_setBadConnection() {
const updateQuery = `UPDATE propertyme.connections SET response = null WHERE name = ?`;
datasource.update("Internal Datastore", updateQuery, [this.connectionName]);
}
/**
* Refreshes the access token using the refresh token.
* @private
* @throws Will throw an error if the token refresh fails.
*/
_refreshToken() {
if (!this.refreshToken) {
throw new Error('No refresh token available');
}
const response = web.form(`https://login.propertyme.com/connect/token`, {
'Content-Type': 'application/x-www-form-urlencoded',
}, {
"grant_type": "refresh_token",
"refresh_token": this.refreshToken,
"client_id": this.CLIENT_ID,
"client_secret": this.CLIENT_SECRET,
});
if (response.status >= 200 && response.status < 300) {
const tokens = JSON.parse(response.body);
this.accessToken = tokens.access_token;
this.refreshToken = tokens.refresh_token || this.refreshToken;
this.expiresIn = Math.floor(Date.now() / 1000) + tokens.expires_in;
this._saveTokens(tokens);
return tokens;
} else {
this._setBadConnection();
console.log(`Token refresh failed. Status ${response.status}. Body:`, response.body);
throw new Error(`Token refresh failed with status ${response.status}`);
}
}
/**
* Checks if the current token is expired or near expiry. (They last 1 hour)
* @private
*/
_isTokenExpired() {
if (!this.accessToken || !this.expiresIn)
return true;
const now = Math.floor(Date.now() / 1000);
return now >= this.expiresIn - (60 * 5); // Refresh 5 minutes before expiry
}
/**
* Makes an HTTP request to the PropertyMe API.
* @private
* @param {string} url - The endpoint URL (relative to the base URL).
* @param {string} [method='GET'] - The HTTP method to use.
* @param {object|null} [body=null] - The request body for POST, PATCH, PUT, DELETE requests.
* @param {number} [retryAttempt=0] - The number of times this request has been retried.
* @returns {object} The API response.
* @throws Will throw an error if the request fails or if there is a server error.
*/
_fetch(url, method = 'GET', body = null, retryAttempt = 0) {
const absoluteUrl = `${this.baseUrl}/${url}`;
const headers = {
"Authorization": `Bearer ${this.accessToken}`,
"Accept": "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._refreshToken();
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);
this._setBadConnection();
throw new Error(`Request failed with status ${response.status}`);
}
}
fetch(url, method = 'GET', body = null) {
return this._fetch(url, method, body);
}
}Process Code
js
script.library("api/propertyme.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.');
try {
const api = new PropertyMeApi("CONNECTOR_NAME");
// See the PropertyMe Swagger docs for available endpoints.
const properties = api.fetch("properties");
console.log(properties);
} catch (e) {
console.log("Something went wrong");
console.log(e);
}
}
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.');
}