UNPKG

11.8 kBSource Map (JSON)View Raw
1{"version":3,"names":[],"mappings":"","sources":["Requestable.js"],"sourcesContent":["/**\n * @file\n * @copyright 2016 Yahoo Inc.\n * @license Licensed under {@link https://spdx.org/licenses/BSD-3-Clause-Clear.html BSD-3-Clause-Clear}.\n * Github.js is freely distributable.\n */\n\nimport axios from 'axios';\nimport debug from 'debug';\nimport {Base64} from 'js-base64';\n\nconst log = debug('github:request');\n\n/**\n * The error structure returned when a network call fails\n */\nclass ResponseError extends Error {\n /**\n * Construct a new ResponseError\n * @param {string} message - an message to return instead of the the default error message\n * @param {string} path - the requested path\n * @param {Object} response - the object returned by Axios\n */\n constructor(message, path, response) {\n super(message);\n this.path = path;\n this.request = response.config;\n this.response = (response || {}).response || response;\n this.status = response.status;\n }\n}\n\n/**\n * Requestable wraps the logic for making http requests to the API\n */\nclass Requestable {\n /**\n * Either a username and password or an oauth token for Github\n * @typedef {Object} Requestable.auth\n * @prop {string} [username] - the Github username\n * @prop {string} [password] - the user's password\n * @prop {token} [token] - an OAuth token\n */\n /**\n * Initialize the http internals.\n * @param {Requestable.auth} [auth] - the credentials to authenticate to Github. If auth is\n * not provided request will be made unauthenticated\n * @param {string} [apiBase=https://api.github.com] - the base Github API URL\n * @param {string} [AcceptHeader=v3] - the accept header for the requests\n */\n constructor(auth, apiBase, AcceptHeader) {\n this.__apiBase = apiBase || 'https://api.github.com';\n this.__auth = {\n token: auth.token,\n username: auth.username,\n password: auth.password,\n };\n this.__AcceptHeader = AcceptHeader || 'v3';\n\n if (auth.token) {\n this.__authorizationHeader = 'token ' + auth.token;\n } else if (auth.username && auth.password) {\n this.__authorizationHeader = 'Basic ' + Base64.encode(auth.username + ':' + auth.password);\n }\n }\n\n /**\n * Compute the URL to use to make a request.\n * @private\n * @param {string} path - either a URL relative to the API base or an absolute URL\n * @return {string} - the URL to use\n */\n __getURL(path) {\n let url = path;\n\n if (path.indexOf('//') === -1) {\n url = this.__apiBase + path;\n }\n\n let newCacheBuster = 'timestamp=' + new Date().getTime();\n return url.replace(/(timestamp=\\d+)/, newCacheBuster);\n }\n\n /**\n * Compute the headers required for an API request.\n * @private\n * @param {boolean} raw - if the request should be treated as JSON or as a raw request\n * @param {string} AcceptHeader - the accept header for the request\n * @return {Object} - the headers to use in the request\n */\n __getRequestHeaders(raw, AcceptHeader) {\n let headers = {\n 'Content-Type': 'application/json;charset=UTF-8',\n 'Accept': 'application/vnd.github.' + (AcceptHeader || this.__AcceptHeader),\n };\n\n if (raw) {\n headers.Accept += '.raw';\n }\n headers.Accept += '+json';\n\n if (this.__authorizationHeader) {\n headers.Authorization = this.__authorizationHeader;\n }\n\n return headers;\n }\n\n /**\n * Sets the default options for API requests\n * @protected\n * @param {Object} [requestOptions={}] - the current options for the request\n * @return {Object} - the options to pass to the request\n */\n _getOptionsWithDefaults(requestOptions = {}) {\n if (!(requestOptions.visibility || requestOptions.affiliation)) {\n requestOptions.type = requestOptions.type || 'all';\n }\n requestOptions.sort = requestOptions.sort || 'updated';\n requestOptions.per_page = requestOptions.per_page || '100'; // eslint-disable-line\n\n return requestOptions;\n }\n\n /**\n * if a `Date` is passed to this function it will be converted to an ISO string\n * @param {*} date - the object to attempt to coerce into an ISO date string\n * @return {string} - the ISO representation of `date` or whatever was passed in if it was not a date\n */\n _dateToISO(date) {\n if (date && (date instanceof Date)) {\n date = date.toISOString();\n }\n\n return date;\n }\n\n /**\n * A function that receives the result of the API request.\n * @callback Requestable.callback\n * @param {Requestable.Error} error - the error returned by the API or `null`\n * @param {(Object|true)} result - the data returned by the API or `true` if the API returns `204 No Content`\n * @param {Object} request - the raw {@linkcode https://github.com/mzabriskie/axios#response-schema Response}\n */\n /**\n * Make a request.\n * @param {string} method - the method for the request (GET, PUT, POST, DELETE)\n * @param {string} path - the path for the request\n * @param {*} [data] - the data to send to the server. For HTTP methods that don't have a body the data\n * will be sent as query parameters\n * @param {Requestable.callback} [cb] - the callback for the request\n * @param {boolean} [raw=false] - if the request should be sent as raw. If this is a falsy value then the\n * request will be made as JSON\n * @return {Promise} - the Promise for the http request\n */\n _request(method, path, data, cb, raw) {\n const url = this.__getURL(path);\n\n const AcceptHeader = (data || {}).AcceptHeader;\n if (AcceptHeader) {\n delete data.AcceptHeader;\n }\n const headers = this.__getRequestHeaders(raw, AcceptHeader);\n\n let queryParams = {};\n\n const shouldUseDataAsParams = data && (typeof data === 'object') && methodHasNoBody(method);\n if (shouldUseDataAsParams) {\n queryParams = data;\n data = undefined;\n }\n\n const config = {\n url: url,\n method: method,\n headers: headers,\n params: queryParams,\n data: data,\n responseType: raw ? 'text' : 'json',\n };\n\n log(`${config.method} to ${config.url}`);\n const requestPromise = axios(config).catch(callbackErrorOrThrow(cb, path));\n\n if (cb) {\n requestPromise.then((response) => {\n if (response.data && Object.keys(response.data).length > 0) {\n // When data has results\n cb(null, response.data, response);\n } else if (config.method !== 'GET' && Object.keys(response.data).length < 1) {\n // True when successful submit a request and receive a empty object\n cb(null, (response.status < 300), response);\n } else {\n cb(null, response.data, response);\n }\n });\n }\n\n return requestPromise;\n }\n\n /**\n * Make a request to an endpoint the returns 204 when true and 404 when false\n * @param {string} path - the path to request\n * @param {Object} data - any query parameters for the request\n * @param {Requestable.callback} cb - the callback that will receive `true` or `false`\n * @param {method} [method=GET] - HTTP Method to use\n * @return {Promise} - the promise for the http request\n */\n _request204or404(path, data, cb, method = 'GET') {\n return this._request(method, path, data)\n .then(function success(response) {\n if (cb) {\n cb(null, true, response);\n }\n return true;\n }, function failure(response) {\n if (response.response.status === 404) {\n if (cb) {\n cb(null, false, response);\n }\n return false;\n }\n\n if (cb) {\n cb(response);\n }\n throw response;\n });\n }\n\n /**\n * Make a request and fetch all the available data. Github will paginate responses so for queries\n * that might span multiple pages this method is preferred to {@link Requestable#request}\n * @param {string} path - the path to request\n * @param {Object} options - the query parameters to include\n * @param {Requestable.callback} [cb] - the function to receive the data. The returned data will always be an array.\n * @param {Object[]} results - the partial results. This argument is intended for internal use only.\n * @return {Promise} - a promise which will resolve when all pages have been fetched\n * @deprecated This will be folded into {@link Requestable#_request} in the 2.0 release.\n */\n _requestAllPages(path, options, cb, results) {\n results = results || [];\n\n return this._request('GET', path, options)\n .then((response) => {\n let thisGroup;\n if (response.data instanceof Array) {\n thisGroup = response.data;\n } else if (response.data.items instanceof Array) {\n thisGroup = response.data.items;\n } else {\n let message = `cannot figure out how to append ${response.data} to the result set`;\n throw new ResponseError(message, path, response);\n }\n results.push(...thisGroup);\n\n const nextUrl = getNextPage(response.headers.link);\n if(nextUrl) {\n if (!options) {\n options = {};\n }\n options.page = parseInt(\n nextUrl.match(/([&\\?]page=[0-9]*)/g)\n .shift()\n .split('=')\n .pop()\n );\n if (!(options && typeof options.page !== 'number')) {\n log(`getting next page: ${nextUrl}`);\n return this._requestAllPages(nextUrl, options, cb, results);\n }\n }\n\n if (cb) {\n cb(null, results, response);\n }\n\n response.data = results;\n return response;\n }).catch(callbackErrorOrThrow(cb, path));\n }\n}\n\nmodule.exports = Requestable;\n\n// ////////////////////////// //\n// Private helper functions //\n// ////////////////////////// //\nconst METHODS_WITH_NO_BODY = ['GET', 'HEAD', 'DELETE'];\nfunction methodHasNoBody(method) {\n return METHODS_WITH_NO_BODY.indexOf(method) !== -1;\n}\n\nfunction getNextPage(linksHeader = '') {\n const links = linksHeader.split(/\\s*,\\s*/); // splits and strips the urls\n return links.reduce(function(nextUrl, link) {\n if (link.search(/rel=\"next\"/) !== -1) {\n return (link.match(/<(.*)>/) || [])[1];\n }\n\n return nextUrl;\n }, undefined);\n}\n\nfunction callbackErrorOrThrow(cb, path) {\n return function handler(object) {\n let error;\n if (object.hasOwnProperty('config')) {\n const {response: {status, statusText}, config: {method, url}} = object;\n let message = (`${status} error making request ${method} ${url}: \"${statusText}\"`);\n error = new ResponseError(message, path, object);\n log(`${message} ${JSON.stringify(object.data)}`);\n } else {\n error = object;\n }\n if (cb) {\n log('going to error callback');\n cb(error);\n } else {\n log('throwing error');\n throw error;\n }\n };\n}\n"],"file":"Requestable.js"}
\No newline at end of file