Skip to content

Instantly share code, notes, and snippets.

@adshrc
Created April 26, 2020 17:44
Show Gist options
  • Save adshrc/1b4f508a0d777b02ef11d7078e182a12 to your computer and use it in GitHub Desktop.
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!
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;
}
}
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