I’ve written a few articles about JavaScript proxying on here, and I’m a big fan. I also use a lot of APIS, and it can be time consuming to keep on checking the REST documentation for how to call them and deal with the UrlFetch responses. SuperFetch is a proxy for UrlFetchApp to help.
What is SuperFetch
SuperFetch is a proxy for UrlFetchApp which has these extra capabilities – most of which I use for all APIS
You’ll need the bmSuperFetch and bmUnitTest libraries – details at the end.
Testing
Throughout this article I’ll use Simple but powerful Apps Script Unit Test library to demonstrate calls and responses. It should be straightforward to see how this works and the responsese to expect from calls
test API
I originally create superFetch to use as a plugin replacement for UrlFetch to my api library (I have one library that I use to access all the APIS I use regularily – mainly Google ones). For this article, though I’ll use the Open Movie Database API (omd) and move onto some more complex APIS in later articles.
The omd needs an api key and a query specifed as url parameters.
A SuperFetch instance
Before we start, let’s set up a few constants to use throughought the test
// marker for missing arguments eg func (_, arg)
const _ = undefined
// the OMDB api endpoint
const omdEndPoint = 'http://www.omdbapi.com'
// omd api key -- get your own!
const omdApiKey = 'xxxxxxxx'
// we'll use this query throughout
const query = 't=Casino'
// make up the relative url params
const url = `?${query}&${apiKey}`
// and the title property of the expected response
const title = "Casino"
some consts for the tests
Test Framework
This demo will all happen with a unit test section that looks like this
// get a unit test instance
const unit = new bmUnitTester.Unit({ showErrorsOnly: true })
// section housing many tests
unit.section (()=> {
.... demo tests
}, {
description: 'open mdb using vanilla superfetch',
})
test framework
A minimal Superfetch
SuperFetch is dependency free, so you’ll need to give it UrlFetchApp to fiddle with. This instance will have nothing fancy like caching yet.
// a minimal superfetch
let superFetch = new bmSuperFetch.SuperFetch({
fetcherApp: UrlFetchApp
})
minimal superfetch
A fetcher
Now we need to create a fetcher specific to the API we want to access. The same SuperFetch can be used with multiple APIS. This fetcher is a proxy for UrlFetch, but with added features
// make a fetcher
let fetcher = superFetch.proxy({
endPoint: omdEndPoint
})
fetcher
A fetcher response
Much of the faffing around with UrlFetch is converting to JSON, checking for and handling failure etc. SuperFetch does all that and returns something that can generally be used as a oneLiner as you’ll see in this demo. The response always looks like this.
/**
* This is the response from the apply proxied function
* @typedef PackResponse
* @property {boolean} cached whether item was retrieved from cache
* @property {object||null} data parsed data
* @property {number} age age in ms of the cached data
* @property {Blob|| null} blob the recreated blob if there was one
* @property {boolean} parsed whether the data was parsed
* @property {HttpResponse} response fetch response
* @property {function} throw a function to throw on error
* @property {Error || string || null} the error if there was one
* @property {number} responseCode the http response code
*/
PackResponse
The regular HttpResponse from UrlFetch is a property of the response should you need to reference it, but since most APIS will be returning JSON, you’ll find the useful stuff already parsed in the data property. We’ll get to the other properties as we go along.
These first 2 tests check that the call worked, and that the data.Title is as expected
unit.is(200, fetcher(url).responseCode, {
description: 'check we code a good response code'
})
unit.is(title, fetcher(url).data.Title, {
description: 'check we found the film'
})
simple fetches
throw method
There’s an additional method returned from superFetch – .throw(). If you add this to the fetch request it’ll throw an error if it detects one, otherwise it’ll just return the response as usual. This allows for easy one liner testing.
unit.is(title, fetcher(url).throw().data.Title, {
description: 'an invisible throw when no errors'
})
pass through throw() method
Testing for thrown error
Unit test can also check for an error being thrown. In this example, we’ll send an invalid api key and check that an error was thrown and it was the error we expected.
unit.is('Invalid API key!',
unit.threw(() => fetcher(url 'rubbish').throw()).Error, {
description: 'should throw'
})
testing for a throw
Caching
SuperFetch has caching built in using Apps script caching with compression and enhanced size limitations.
The current instance doesn’t have caching enabled, so a fetch will not be found in cache.
unit.is(false, fetcher(url).cached, {
description: 'no caching turned on'
})
no caching yet
Here’s how to create a caching instance – passing the Apps Script CacheService of your choice.
// add caching
superFetch = new bmSuperFetch.SuperFetch({
fetcherApp: UrlFetchApp,
cacheService: CacheService.getUserCache()
})
// make a cached fetcher
fetcher = superFetch.proxy({
endPoint: omdEndPoint
})
fetcher with caching
Fetches using this fetcher will write and read from cache in preference to going to to the API
unit.is(title, fetcher(url).data.Title, {
description: 'check we found the film'
})
unit.is(true, fetcher(url).cached, {
description: 'caching now turned on'
})
caching is now on
Fake HttpResponse response
If an item has been retrieved to cache it of course won’t have an httpResponse available (should you need to access the headers etc), but SuperFetch creates a fake one based on the values of the original httpResponse
unit.is(200, fetcher(url).responseCode, {
description: 'even though its from cache we still get a reponse code'
})
unit.is('object', typeof fetcher(url).response.getHeaders(), {
description: 'even though its from cache we still have access to some response functions'
})
fake reponse
Throttling
Some APIS need you to throttle calls. SuperFetch uses Rate limit handler and helper: Test and manage rate limiting APIS to throttle calls if you need this.
Let’s say an API doesn’t want to you make calls more than every 2 seconds. Here’s how.
// with throttling && nocaching
let delay = 2000
fetcher = new bmSuperFetch.SuperFetch({
fetcherApp: UrlFetchApp,
rottler: bmSuperFetch.Libraries.bmRottler.newRottler({
delay
})
}).proxy({
endPoint: omdEndPoint
})
throttling
Sub Libraries
You’ll notice that there is a bmSuperFetch.Libraries property. This allows you to access any of the libraries used by SuperFetch directly – so you don’t have to bother adding a reference to them in your own script. In other words, in the fragment of code above, you’re able to access the bmRottler library via bmSuperFetch.Libraries.bmRottler.
Throttling delay demo
Here there should be at least 2000ms between each call
let start = new Date().getTime()
unit.is(false, fetcher(url).cached, {
description: 'no caching turned on'
})
unit.is(true, (() => {
fetcher(url)
return (new Date().getTime() - start) > delay
})(), {
description: `should be at least ${delay}ms since last call`
})
throttle
Rate limiting
This is a bit different that delay. This is to deal with the case where an API says it will accept only ‘n’ calls in a period. For example 100 calls an hour.
Delay can also be combined with rate limiting – so in this example we’re allowing 2 calls every 4 seconds, with each call at lease 500ms apart. I’ve also overridden the sleep function, so we can see exactly how long it is waiting till it makes the next call
let period = 4000
let rate = 2
delay = 500
fetcher = new bmSuperFetch.SuperFetch({
fetcherApp: UrlFetchApp,
rottler: bmSuperFetch.Libraries.bmRottler.newRottler({
delay,
period,
rate,
sleep: (ms) => {
// override the default sleeper and watch how long we're sleeping for
console.log('sleeping for', ms)
Utilities.sleep(ms)
}
})
}).proxy({
endPoint: omdEndPoint
})
rate limiting and delay
Here’s the tests
start = new Date().getTime()
unit.is(title, fetcher(url).data.Title, {
description: 'starting rate limiting 1'
})
unit.is(title, fetcher(url).data.Title, {
description: 'starting rate limiting 2'
})
unit.is(title, fetcher(url).data.Title, {
description: 'starting rate limiting3'
})
unit.is(true, new Date().getTime() - start > period, {
description: `should be at least ${period delay}ms start to finish`
})
rate and delay demo
Complete test set
For convenience, here’s all of those tests wrapped in a test section
const _ = undefined
const omdEndPoint = 'http://www.omdbapi.com'
const omdApiKey = 'xxxx'
unit.section(() => {
// a minimal superfetch
let superFetch = new bmSuperFetch.SuperFetch({
fetcherApp: UrlFetchApp
})
// make a fetcher
let fetcher = superFetch.proxy({
endPoint: omdEndPoint
})
const apiKey = `apiKey=${omdApiKey}`
const query = 't=Casino'
const url = `?${query}&${apiKey}`
const title = "Casino"
unit.is(200, fetcher(url).responseCode, {
description: 'check we code a good response code'
})
unit.is(title, fetcher(url).data.Title, {
description: 'check we found the film'
})
unit.is(title, fetcher(url).throw().data.Title, {
description: 'an invisible throw when no errors'
})
unit.is('Invalid API key!',
unit.threw(() => fetcher(url 'rubbish').throw()).Error, {
description: 'should throw'
})
unit.is(false, fetcher(url).cached, {
description: 'no caching turned on'
})
// add caching
superFetch = new bmSuperFetch.SuperFetch({
fetcherApp: UrlFetchApp,
cacheService: CacheService.getUserCache()
})
// make a cached fetcher
fetcher = superFetch.proxy({
endPoint: omdEndPoint
})
unit.is(title, fetcher(url).data.Title, {
description: 'check we found the film'
})
unit.is(true, fetcher(url).cached, {
description: 'caching now turned on'
})
unit.is(200, fetcher(url).responseCode, {
description: 'even though its from cache we still get a reponse code'
})
unit.is('object', typeof fetcher(url).response.getHeaders(), {
description: 'even though its from cache we still have access to some response functions'
})
// with throttling && nocaching
let delay = 2000
fetcher = new bmSuperFetch.SuperFetch({
fetcherApp: UrlFetchApp,
rottler: bmSuperFetch.Libraries.bmRottler.newRottler({
delay
})
}).proxy({
endPoint: omdEndPoint
})
let start = new Date().getTime()
unit.is(false, fetcher(url).cached, {
description: 'no caching turned on'
})
unit.is(true, (() => {
fetcher(url)
return (new Date().getTime() - start) > delay
})(), {
description: `should be at least ${delay}ms since last call`
})
// with rate limiting
let period = 4000
let rate = 2
delay = 500
fetcher = new bmSuperFetch.SuperFetch({
fetcherApp: UrlFetchApp,
rottler: bmSuperFetch.Libraries.bmRottler.newRottler({
delay,
period,
rate,
sleep: (ms) => {
// override the default sleeper and watch how long we're sleeping for
console.log('sleeping for', ms)
Utilities.sleep(ms)
}
})
}).proxy({
endPoint: omdEndPoint
})
start = new Date().getTime()
unit.is(title, fetcher(url).data.Title, {
description: 'starting rate limiting 1'
})
unit.is(title, fetcher(url).data.Title, {
description: 'starting rate limiting 2'
})
unit.is(title, fetcher(url).data.Title, {
description: 'starting rate limiting3'
})
unit.is(true, new Date().getTime() - start > period, {
description: `should be at least ${period delay}ms start to finish`
})
}, {
description: 'open mdb using vanilla superfetch'
})
complete test set
Making an API class
For illustration, let’s make a simple API class based on SuperFetch to access the omd api. In later articles I’ll demonstrate more complex APIs to access the Google REST APIS for gcs, drv, iam and others. They’ll all follow the same pattern as this simple demo, but of course with more methods and capabilities.
OmdApi class
This class can substitute for the fetcher in the previous examples
/**
* @typedef OmdOptions
* @property {_SuperFetch} superFetch a superfetch instance
* @property {boolean} noCache whether to cache
* @property {boolean} showUrl whether to showUrls when fetching
* @property {object[]} extraParams always add these params
*/
class _OmdApi {
/**
* @param {OmdOptions}
* @return {_OmdApi}
*/
constructor({
superFetch,
// caching can be used here if you don't want to keep calling the iam service for each call
// but of course they do expire
noCache = false,
showUrl,
extraParams = []
}) {
this.extraParams = Utils.arrify(extraParams)
this.proxy = superFetch.proxy({
endPoint: `http://www.omdbapi.com`,
noCache,
showUrl
})
}
makePath({ path = '', params }) {
return Utils.makeUrl({
url: Utils.makepath({ path, base: '' }),
params: params.concat(this.extraParams)
})
}
get({path=''} = {}, ...params) {
return this.proxy(this.makePath({path, params }))
}
}
var OmdApi = _OmdApi
OmdApi
Using the OmdApi
Here’s a few examples
unit.section(() => {
let superFetch = new bmSuperFetch.SuperFetch({
fetcherApp: UrlFetchApp,
cacheService: CacheService.getUserCache()
})
// make an api
const omd = new OmdApi({
superFetch,
extraParams: {
apiKey: omdApiKey
}
})
const title = "Casino"
const query = {t:title}
unit.is(200, omd.get(_,query).responseCode, {
description: 'check we code a good response code'
})
unit.is(title, omd.get(_,query).data.Title, {
description: 'check we found the film'
})
unit.is(title, omd.get(_,query).throw().data.Title, {
description: 'an invisible throw when no errors'
})
unit.is(true, omd.get(_,query).throw().cached, {
description: 'caching turned on'
})
// lets make a noncache version of a fetcher
const omdNoCache = new OmdApi({
noCache: true,
superFetch,
extraParams: {
apiKey: omdApiKey
}
})
unit.is(false, omdNoCache.get(_,query).cached, {
description: 'now no cache'
})
unit.is(false, omd.get(_,query).cached, {
description: 'it also cleared the cache from the other cacher'
})
unit.is(true, omd.get(_,query).cached, {
description: 'but now its back in cache again'
})
unit.is(true, omd.get(_,query).age < 1000, {
description: 'you can see even how long ago the cache entry was written'
})
}, {
description: 'open mdb using omdApi'
})
some example for omdApi
What’s next
In future articles we’ll look at other Apis built on SuperFetch, as well as built in recovery from Rate Limit errors, authentication and impersonation. But that’s it for now
Links
bmSuperFetch: 1B2scq2fYEcfoGyt9aXxUdoUPuLy-qbUC2_8lboUEdnNlzpGGWldoVYg2
IDE
GitHub
bmUnitTester: 1zOlHMOpO89vqLPe5XpC-wzA9r5yaBkWt_qFjKqFNsIZtNJ-iUjBYDt-x
IDE
GitHub
Related
The Drive API offers a whole range of conversions between mimeTypes, but it's a little fiddly to figure out exactly ...
I've written many times about various Apps Script caching techniques such as how to deal with size limits and use ...
Motivation I've been working on a CardService Add-on lately which also uses HtmlService, and also runs quite a few things ...
Smg is a SuperFetch plugin to access the Google Cloud Secrets API. SuperFetch is a proxy for UrlFetchApp with additional ...
SuperFetch is a proxy for UrlFetchApp with additional features such as built-in caching – see SuperFetch – a proxy enhancement ...
Tank and Drv are SuperFetch plugins to emulate streaming and use the Drive REST API with Apps Script. SuperFetch is ...
Tank and Drv are SuperFetch plugins to emulate streaming and use the Drive REST API with Apps Script. SuperFetch is ...
Tank is a SuperFetch plugin to emulate streaming with Apps Script. SuperFetch is a proxy for UrlFetchApp with additional features ...
Drv is a SuperFetch plugin to access the Google Drive API. SuperFetch is a proxy for UrlFetchApp with additional features ...
Twt is a SuperFetch plugin to easily access to the Twitter v2 API. SuperFetch is a proxy for UrlFetchApp with ...
Twt is a SuperFetch plugin to easily access to the Twitter v2 API. SuperFetch is a proxy for UrlFetchApp with ...
I covered how to handle the somewhat more complex OAUTH2 authorization flow for the Twitter v2 API (OAuth 2.0 Authorization ...
Twt is a SuperFetch plugin to easily access to the Twitter v2 API. SuperFetch is a proxy for UrlFetchApp with ...
Motivation Goa is a library to support OAuth2 for Apps Script connecting to a variety of services, using a variety ...
It's been a few years since I first created the Goa library. Initially it was mainly to provide OAuth2 authorization ...
Frb is a SuperFetch plugin to easily access a Firebase Real time database. SuperFetch is a proxy for UrlFetchApp with ...
SuperFetch is a proxy for UrlFetchApp with additional features - see SuperFetch - a proxy enhancement to Apps Script UrlFetch ...
I've written a few articles about JavaScript proxying on here, and I'm a big fan. I also use a lot ...
Motivation Caching is a great way to improve performance, avoid rate limit problems and even save money if you are ...
Why unit testing? There are many test packages for Node (my favorite is ava) and there are also a few ...