Created
April 26, 2020 17:44
-
-
Save adshrc/1b4f508a0d777b02ef11d7078e182a12 to your computer and use it in GitHub Desktop.
Google Apps Script (GAS) Query Library: Extend it to query your desired RESTful service. You can see an example for Harvest below. Use clasp for compiling TypeScript Code!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export namespace Harvest { | |
export class BaseQuery extends Query.BaseQuery { | |
constructor(path: string, expectedStatusCodes: Query.ExpectedStatusCodes) { | |
// get global properties | |
const SCRIPT_PROPS = PropertiesService.getScriptProperties(); | |
const HARVEST_TOKEN = SCRIPT_PROPS.getProperty('HARVEST_TOKEN'); | |
const HARVEST_ACCOUNT_ID = SCRIPT_PROPS.getProperty('HARVEST_ACCOUNT_ID'); | |
const HARVEST_BASE_URL = SCRIPT_PROPS.getProperty('HARVEST_BASE_URL'); | |
let url = HARVEST_BASE_URL + '/' + path; | |
let headers: GoogleAppsScript.URL_Fetch.HttpHeaders = { | |
'User-Agent': 'VISENDA', | |
'Authorization': 'Bearer ' + HARVEST_TOKEN, | |
'Harvest-Account-ID': HARVEST_ACCOUNT_ID | |
}; | |
super(url, headers, expectedStatusCodes); | |
} | |
/** | |
* handles queries over multiple pages and returns the objects without query meta | |
* Returns an array of Objects if dataProp is set, else a single Object | |
* | |
* @param url The URL to query | |
* @param options The URLFetchRequestOptions (headers, etc.) | |
* @param expectedStatusCode The expected numerical status code (eg. 200) | |
* @param dataProp (optional) The property which holds the data Objects (e.g. users) | |
*/ | |
processQuery(url: string, options: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions, expectedStatusCode: number, dataProp?: string): Object | Object[] { | |
// harvest returns either a single object | |
// or array of objects, which are hold by a specific object property (e.g. projects) | |
let result: Object[] = []; | |
// extract data from Objects/Arrays dataProp Property (if set) | |
if (dataProp) { | |
let pageResult: Object | Object[]; | |
let pageResultKeys = []; | |
do { | |
try { | |
// expect a raw query here, since we dont set the dataProp | |
// we can't use the super functions dataProp "feature" since we need the page link below | |
pageResult = super.processQuery(url, options, expectedStatusCode); | |
// get the pageResultKeys to check if dataProp is included | |
pageResultKeys = Object.keys(pageResult); | |
if (pageResultKeys.includes(dataProp)) { | |
// concat page result | |
result = result.concat(pageResult[dataProp]); | |
} else { | |
throw Error('[HARVEST-QUERY] "' + dataProp + '" is not a key in "' + pageResultKeys + '"!'); | |
} | |
// use next page url to query in next cycle | |
url = pageResult['links'] ? pageResult['links'].next : null; | |
} catch (e) { | |
throw Error('[HARVEST-QUERY] Could not extract data from raw query: ' + e) | |
} | |
} while (url); | |
} else { | |
// just put the single query result in an array to unify the return | |
result = super.processQuery(url, options, expectedStatusCode); | |
} | |
return result; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export namespace Query { | |
/** | |
* Represents a basic query via the GAS fetch function | |
*/ | |
export class BaseQuery { | |
/** | |
* The BaseUrl for queries | |
*/ | |
baseUrl: string; | |
/** | |
* match the query response with these to check if everything is good | |
*/ | |
expectedStatusCodes: ExpectedStatusCodes = { | |
get: 200, | |
delete: 200, | |
patch: 201, | |
post: 201, | |
put: 201 | |
}; | |
/** | |
* options object for the query | |
*/ | |
baseOptions: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = { | |
contentType: 'application/json', | |
headers: { | |
'User-Agent': 'VISENDA GAS', | |
}, | |
payload: {} | |
}; | |
/** | |
* The constructor | |
* | |
* @param baseUrl The Base Url for queries | |
* @param headers The headers object to be used for all Queries (Authentication, etc.) | |
* @param expectedStatusCodes The expected status codes as object | |
*/ | |
constructor(baseUrl: string, headers: GoogleAppsScript.URL_Fetch.HttpHeaders, expectedStatusCodes: ExpectedStatusCodes) { | |
this.baseUrl = baseUrl; | |
// merge default with given headers | |
this.baseOptions['headers'] = { | |
...this.baseOptions['headers'], | |
...headers | |
}; | |
this.expectedStatusCodes = expectedStatusCodes; | |
} | |
/** | |
* The GET Query | |
* | |
* @param id The ID for single object queries (e.g. baseUrl.tld/api/user/ID) | |
* @param urlParams The url Params as [key,value] pairs | |
* @param dataProp The Property to extract data from, out of the query result. | |
*/ | |
get(id?: string, urlParams?: string[][], dataProp?: string): any | any[] { | |
let customOptions: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = { | |
method: 'get' | |
}; | |
let options = this.getOptions(customOptions); | |
let url = this.getUrl(id, urlParams); | |
return this.processQuery(url, options, this.expectedStatusCodes.get, dataProp); | |
} | |
/** | |
* The DELETE Query | |
* | |
* @param id The ID for single object queries (e.g. baseUrl.tld/api/user/ID) | |
* @param urlParams The url Params as [key,value] pairs | |
* @param dataProp The Property to extract data from, out of the query result. | |
*/ | |
delete(id?: string, urlParams?: string[][], dataProp?: string): any | any[] { | |
let customOptions: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = { | |
method: 'delete' | |
}; | |
let options = this.getOptions(customOptions); | |
let url = this.getUrl(id, urlParams); | |
return this.processQuery(url, options, this.expectedStatusCodes.delete, dataProp); | |
} | |
/** | |
* The PATCH Query | |
* | |
* @param payload The Payload Object ("Body") | |
* @param id The ID for single object queries (e.g. baseUrl.tld/api/user/ID) | |
* @param urlParams The url Params as [key,value] pairs | |
* @param dataProp The Property to extract data from, out of the query result. | |
*/ | |
patch(payload?: Object, id?: string, urlParams?: string[][], dataProp?: string): any | any[] { | |
let customOptions: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = { | |
method: 'patch', | |
payload: payload ? JSON.stringify(payload) : null | |
}; | |
let options = this.getOptions(customOptions); | |
let url = this.getUrl(id, urlParams); | |
return this.processQuery(url, options, this.expectedStatusCodes.patch, dataProp); | |
} | |
/** | |
* The POST Query | |
* | |
* @param payload The Payload Object ("Body") | |
* @param id The ID for single object queries (e.g. baseUrl.tld/api/user/ID) | |
* @param urlParams The url Params as [key,value] pairs | |
* @param dataProp The Property to extract data from, out of the query result. | |
*/ | |
post(payload?: Object, id?: string, urlParams?: string[][], dataProp?: string): any | any[] { | |
let customOptions: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = { | |
method: 'post', | |
payload: payload ? JSON.stringify(payload) : null | |
}; | |
let options = this.getOptions(customOptions); | |
let url = this.getUrl(id, urlParams); | |
return this.processQuery(url, options, this.expectedStatusCodes.post, dataProp); | |
} | |
/** | |
* The PUT Query | |
* | |
* @param payload The Payload Object ("Body") | |
* @param id The ID for single object queries (e.g. baseUrl.tld/api/user/ID) | |
* @param urlParams The url Params as [key,value] pairs | |
* @param dataProp The Property to extract data from, out of the query result. | |
*/ | |
put(payload?: Object, id?: string, urlParams?: string[][], dataProp?: string): any | any[] { | |
let customOptions: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = { | |
method: 'put', | |
payload: payload ? JSON.stringify(payload) : null | |
}; | |
let options = this.getOptions(customOptions); | |
let url = this.getUrl(id, urlParams); | |
return this.processQuery(url, options, this.expectedStatusCodes.put, dataProp); | |
} | |
/** | |
* Processes a single query. Return is equal to the remote APIs return (Object or Array of Objects) | |
* Option "method: 'post | put | delete | get | patch' in options Object is mandatory | |
* | |
* @param url The URL to query to | |
* @param options The URLFetchRequestOptions (headers, etc.) | |
* @param expectedStatusCode The expected numerical status code (eg. 200) | |
*/ | |
rawQuery(url: string, options: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions, expectedStatusCode: number): any | any[] { | |
let method = options?.method?.toUpperCase(); | |
let query; | |
try { | |
query = UrlFetchApp.fetch(url, options); | |
} catch (e) { | |
throw Error('[QUERY][' + method + '] Could not make a query to "' + url + '": ' + e); | |
} | |
// if status code is as expected, return the body | |
if (query.getResponseCode() === expectedStatusCode) { | |
let result; | |
try { | |
result = JSON.parse(query.getContentText()); | |
} catch (e) { | |
throw Error('[QUERY] Could not parse JSON from result: ' + e); | |
} | |
return result; | |
} else { | |
let responseCode = query.getResponseCode(); | |
throw Error('[QUERY][' + method + '][' + responseCode + '] Expected status code "' + expectedStatusCode + '" after quering "' + url + '"'); | |
} | |
} | |
/** | |
* Processes/Manipulates data returned from a rawQuery. | |
* Extracts the data from dataProp Property if set. Otherwise returns the raw query. | |
* | |
* @param url The URL to query to | |
* @param options The URLFetchRequestOptions (headers, etc.) | |
* @param expectedStatusCode The expected numerical status code (eg. 200) | |
* @param dataProp The Objects/Array property name, which holds the real data | |
*/ | |
processQuery(url: string, options: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions, expectedStatusCode: number, dataProp?: string): any | any[] { | |
let result: any | any[] = this.rawQuery(url, options, expectedStatusCode); | |
// extract data from Objects/Arrays dataProp Property (if set) | |
if (dataProp) { | |
let keys = Object.keys(result); | |
if (keys.includes(dataProp)) { | |
// extract the result | |
result = result[dataProp]; | |
} else { | |
throw Error('[QUERY] Could not extract the result: "' + dataProp + '" is not a key in "' + keys + '"!'); | |
} | |
} | |
return result; | |
} | |
/** | |
* Returns the full url to use for querying | |
* | |
* @param id The id, which will be used as path (url.com/id) | |
* @param params The params as array of [key,value] pairs | |
*/ | |
getUrl(id?: string, params?: string[][]): string { | |
let url = this.baseUrl; | |
// add id if set | |
url += id ? '/' + id : ''; | |
// add params if set | |
if (params) { | |
url = getParameterizedUrl(url, params); | |
} | |
return url; | |
} | |
/** | |
* Returns the URLFetchRequestOptions for the query | |
* | |
* @param customOptions The custom options for the query. Will be merged with the default ones. | |
*/ | |
getOptions(customOptions: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = {}): GoogleAppsScript.URL_Fetch.URLFetchRequestOptions { | |
let options = {...this.baseOptions, ...customOptions}; | |
return options; | |
} | |
} | |
/** | |
* Returns a parameterized URL | |
* | |
* @param url The Base URL | |
* @param params The Params as 2D string array | |
*/ | |
export function getParameterizedUrl(url: string, params: string[][]) { | |
let resultUrl = url; | |
for (let i = 0; i < params.length; i++) { | |
// prefix | |
resultUrl += (i == 0) ? '?' : '&'; | |
// key=value pair | |
resultUrl += params[i][0] + '=' + params[i][1]; | |
} | |
return resultUrl; | |
} | |
/** | |
* Define expected status codes per query method | |
*/ | |
export interface ExpectedStatusCodes { | |
get: number, | |
delete: number, | |
patch: number, | |
post: number, | |
put: number | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment