UNPKG

canvas-lms-api

Version:

Promise-driven accessor for the Canvas LMS API

163 lines (134 loc) 5.37 kB
import delay from 'delay'; import fetch from 'node-fetch'; import merge from 'lodash.merge'; import parseLink from 'parse-link-header'; import qs from 'qs'; import { resolve } from 'url'; import union from 'lodash.union'; function Canvas(host, options) { options = options || {}; options.json = true; this.name = 'canvas'; this.accessToken = options.accessToken || options.token || ''; this.apiVersion = options.apiVersion || options.version || 'v1'; this.query = options.query || {}; this.host = host; this.qsStringifyOptions = { arrayFormat: 'brackets' }; } function random(low, high) { return low + Math.floor(Math.random() * (high - low + 1)); }; Canvas.prototype._buildApiUrl = function (endpoint) { return resolve(this.host, '/api/' + this.apiVersion + (endpoint[0] === '/' ? '' : '/') + endpoint); }; Canvas.prototype._http = function (options, prevBody) { options.headers = { Authorization: 'Bearer ' + this.accessToken, 'Content-Type': 'application/json' }; // If we've exhausted half the rate limit start delaying our requests so we don't run out the rest let throttle = 0; if (this.rateLimitRemaining && this.rateLimitRemaining < this.rateLimitMax / 2) { throttle = random(50, 100); } const url = `${options.url}${options.qs && qs.stringify(options.qs) !== "" ? "?" + qs.stringify(options.qs, options.qsStringifyOptions): ""}` const myOptions = {...options}; delete myOptions.url; delete myOptions.qs; delete myOptions.qsStringifyOptions; const that = this; return delay(throttle) .then(function () { return fetch(url, myOptions); }) .then(async response => [response, await response.text()]) .then(([response, body]) => { if (!response.ok) { // Check for throttling and retry after a delay if (response.status === 403 && /Rate Limit Exceeded/.test(body)) { // Use a random delay so that we don't retry a ton of requests at the same time and exhaust the limit again return delay(random(200, 500)) .then(function () { return that._http(options, prevBody); }); } const err = new Error(); body = JSON.parse(body); try { err.message = body.errors.map(function (err) { return err.message; }).join('\n'); } catch (e) { err.message = body.message; } err.errors = body.errors; err.code = response.statusCode; throw err; } body = JSON.parse(body); if (!this.rateLimitMax) { this.rateLimitMax = response.headers.get('x-rate-limit-remaining'); } this.rateLimitRemaining = response.headers.get('x-rate-limit-remaining'); if (response.headers.get("link")) { let nextLink = parseLink(response.headers.get("link")); if (nextLink.next) { nextLink = nextLink.next.url.split('?'); const nextOptions = { method: options.method, url: nextLink[0], qs: qs.parse(nextLink[1]), qsStringifyOptions: this.qsStringifyOptions }; return that._http(nextOptions, union(prevBody, body)); } } if (prevBody) { return union(prevBody, body); } return body; }); }; Canvas.prototype._querify = function (query) { return merge({}, this.query, query && typeof query.valueOf() === "string" ? qs.parse(query) : query); }; Canvas.prototype.delete = function (endpoint, querystring, qsStringifyOptions) { const options = { method: 'DELETE', url: this._buildApiUrl(endpoint), qs: this._querify(querystring), qsStringifyOptions: qsStringifyOptions || this.qsStringifyOptions }; return this._http(options); }; Canvas.prototype.get = function (endpoint, querystring, qsStringifyOptions) { const options = { method: 'GET', url: this._buildApiUrl(endpoint), qs: this._querify(querystring), qsStringifyOptions: qsStringifyOptions || this.qsStringifyOptions }; return this._http(options); }; Canvas.prototype.post = function (endpoint, querystring, form, qsStringifyOptions) { const options = { method: 'POST', url: this._buildApiUrl(endpoint), qs: this._querify(querystring), qsStringifyOptions: qsStringifyOptions || this.qsStringifyOptions, body: JSON.stringify(form) }; return this._http(options); }; Canvas.prototype.put = function (endpoint, querystring, form, qsStringifyOptions) { const options = { method: 'PUT', url: this._buildApiUrl(endpoint), qs: this._querify(querystring), qsStringifyOptions: qsStringifyOptions || this.qsStringifyOptions, body: JSON.stringify(form) }; return this._http(options); }; export default Canvas;