A simple GitHub API library for JavaScript that works in both NodeJS and the browser. Features:
- Takes a request-level approach that naturally covers the entire GitHub v3 API.
- Supports the GraphQL v4 API.
- All requests return promises. (You may need to add a polyfill in the browser, depending on your target platforms.)
- Responses are (optionally) cached (segregated by user identity), and requests are conditional to save on bandwidth and request quota. Inspired by simple-github, octo, and octokit.
You need to ensure that an ES2015-compatible Promise class is defined.
Caching is enabled by default but you can override with a custom instance of LRUCache passed as an option to the constructor. If the cache is enabled Hubkit respects Cache-Control headers on the response (that GitHub currently seems to set to 1 minute for all requests), and will return a potentially stale value from the cache unless you specify {fresh: true}.
A simple REST example:
var gh = new Hubkit({
token: '123456890ABCDEF',
owner: 'pkaminski',
repo: 'hubkit'
});
gh.request('GET /repos/:owner/:repo/commits').then(console.log);
gh.request('GET /repos/:owner/:repo/git/commits/:sha', {sha: '09876abc'}).then(console.log);
gh.request('POST /repose/{owner}/{repo}/pulls', {body: {title: 'foo', head: 'bar', base: 'master'}});And one for GraphQL:
// initialize gh as above
gh.graph(`
query ($after: String) {
search (type: ISSUE, first: 10, after: $after, query: `type: pr`) {
pageInfo {hasNextPage, endCursor},
nodes {
... on PullRequest {
number, title
}
}
}
}
`);You issue requests exactly as documented in GitHub's REST API or
GraphQL API. For REST, path segments of the form :foo or
{foo} are interpolated from the options object passed as the second argument and defaulting to the
options object passed to the constructor. The method can be specified either together with the path,
or as a {method: 'GET'} option (the inline one takes precedence, and GET is the default if
nothing else is found).
GraphQL queries are first run through a preprocessor that supports the following directives:
#ghe(minVersion): The following block is excluded if the GHE version is too old.#scope(scope): The following block is excluded if the user's authorization lacks the specified scope.#exists(type[.field]): The following block is excluded if the given type or field does not exist in the schema.#field(type, field1[, field2[, ...]]): Gets substituted with the first given field that exists in the schema for the given type.
This is useful since GraphQL forbids references to fields not in the schema, and GHE servers in the field are often months or years behind github.com in that respect. To use the #ghe or #scope directives you need to include the gheVersion or scopes properties respectively in the options (see below).
Schema information for the #exists and #field directives is queried from the server and is cached indefinitely in memory (regardless of any cache related options).
Here is an example demonstrating each directive:
gh.graph(`
query ($owner: String!, $repo: String!, $number: Int!) {
repository (owner: $owner, name: $repo) {
pullRequest (number: $number) {
id, number, title,
#ghe(2.17) {
isDraft
#}
#exists(PullRequest.mergeQueueEntry) {
mergeQueueEntry {
headCommit {oid}
}
#}
reviewRequests {
nodes {
requestedReviewer {
...on User {
login, name,
id: #field(User, fullDatabaseId, databaseId)
}
#scope(read:org) {
...on Team {combinedSlug, name}
#}
}
}
}
}
}
}
`);There are two ways to authenticate: either pass a token to the options, or both a clientId and
clientSecret. Unauthenticated requests are fine too, of course.
Every call returns a Promise. The returned values are exactly as documented in the GitHub API,
except that requests with option {boolean: true} will return true or false instead (sorry, no
way to automate it). Note that for paged responses, all pages will be concatenated together into
the return value by default (see below).
After every request, you can access rateLimit and rateLimitRemaining (or searchRateLimit and
searchRateLimitRemaining if it's a search request, or graphRateLimit and
graphRateLimitRemaining if it's a GraphQL query) for the latest information on your GitHub
quotas, and oAuthScopes to see what scopes your authorization entitles you to, on your metadata
object (see below) or on Hubkit if you didn't set one.
You can augment a Hubkit instance by calling gh.scope({...moreOptions}) to return a new instance that combines both sets of options.
Hubkit.identify403Error(error) identifies known GitHub 403 causes from a message string or an
object with a message property, including an Error. It accepts raw GitHub messages and messages
prefixed by Hubkit or a server response wrapper. Unknown messages return undefined.
It only examines the message; the caller should check the HTTP status as appropriate. The quota
patterns can also be used for 429 errors.
The result contains a stable, detailed code. Authentication and access failures also include a
broad category and a concise error description. Quota failures instead have quota: true and
no category or error, so callers can supply their own retry guidance.
code |
category |
error |
quota |
|---|---|---|---|
account-suspended |
badauth |
GitHub account suspended | |
email-unverified |
badauth |
Email address not verified | |
saml-enforcement |
badauth |
Incomplete SAML authorization | |
admin-required |
badauth |
No admin rights | |
two-factor-required |
badauth |
Two-factor authentication not set up | |
oauth-app-restrictions |
thirdparty |
Third-party app restrictions in effect | |
ip-allow-list |
iprestricted |
GitHub IP allow list blocks access | |
access-blocked |
notfound |
Repository access blocked | |
secondary-rate-limit |
true |
||
rate-limit |
true |
const reason = Hubkit.identify403Error(error);
if (reason?.category) {
// Consumers using broad error codes can retain their existing representation.
return {code: reason.category, error: reason.error};
}The detailed code distinguishes causes for diagnostics or error grouping without including
request URLs or organization names. The rate-limit code covers other rate-limit, request-quota,
and abuse-detection messages.
For HTTP status codes 400 and above, Hubkit reads the response body as text, regardless of
media or responseType. In the error passed to onError or rejected by the request promise:
error.response.rawDatais the body as text.error.response.datais the parsed JSON value when the response Content-Type isapplication/jsonor anapplication/*+jsontype. Otherwise it is the body as text.- Malformed JSON and empty bodies remain text, preserving the HTTP status and original text instead of replacing the HTTP error with a JSON parsing error.
Successful responses keep their requested representation. When upgrading from 8.x, update any
error handlers that expect a Blob or ArrayBuffer in error.response.data or
error.response.rawData; those fields now contain parsed JSON or text as described above.
This also applies to handlers that recover from an HTTP error by returning a value from onError.
Valid options to pass (to the constructor or to each request), or to set on Hubkit.defaults,
include:
token: String token to use for authentication; takes precedence over other auth methods.clientIdandclientSecret: For app-based anonymous authentication (increased API quotas without impersonating a user).userAgent: The user-agent to present in requests. Uses the browser's user agent, orHubkitin NodeJS.host: The URL to prepend to all request paths; defaults tohttps://api.github.com.graphHost: The URL to use for all GraphQL requests; defaults to using the value ofhostwhich works fine forgithub.com, but you'll need to set a separate value when working with GitHub Enterprise.timeout: The timeout in milliseconds to apply to the request; none by default. If the timeout is reached, the request will abort with aTimeoutError.cache: An instance of LRUCache. The objects inserted into the cache will be of the form{value: {...}, eTag: 'abc123', status: 200, headers: {...}, size: 1763, expiry: 1770853094}. You can use the (approximate)sizefield to help your cache determine when to evict items, but note that it tends to underestimate the actual size size of the object by 3-4x. The default cache is set to hold ~10MB of the measured bytes amount (so ~30-40MB of actual memory usage).fresh: If true, force a request to be issued to the server even if a cache is in use and an unexpired value available. This is different from turning off the cache for the request since it can still make use of ETags and get a cheap 304 response in return.maxItemSizeRatio: The maximum ratio of the size of any single item to the size of the cache, to avoid blowing away the entire cache with one huge item. The default is set to 0.1, limiting each item to at most 1/10th the max size of the cache.stats: Reports the cache hit rate viahitRate(number of items hit / total attempted) andhitSizeRate(total size of items hit / total attempted) attributes. You canreset()the stats to start counting from scratch again. A default instance is set onHubkit.defaultsbut you can also assign anew Hubkit.Stats()to aHubkitinstance if you prefer.immutable: If true, indicates that the return value for this call is immutable, so if it's available in the cache it can be reused without sending a request to GitHub to check freshness.stale: If true, any cached value is considered acceptable, even if it has expired.method: The HTTP method to use for the request.media: A GitHub-specific media type for the response content. Valid values are:- for comment bodies:
raw+json(default),text+json,html+json,full+json - for blobs:
json(default),raw - for commits, etc.:
diff,patch
- for comment bodies:
body: The contents of the request to send, typically a JSON-friendly object.variables: For GraphQL queries, variables to pass to the server along with the query.autoQueryRateLimit: For GraphQL queries, whether to inject arateLimit {cost, remaining}property into every query. This is used to figure out the cost information passed toonReceive(see below).responseType: The response type if you want to receive raw data; one oftext,arraybuffer, orblob. Only useful when fetching file blobs. Applies to successful responses only; HTTP error responses ignore this option (see HTTP error bodies).perPage: The number of items to return per page of response. Defaults to 100.allPages: Whether to automatically fetch all pages by following thenextlinks and concatenate the results before returning them. Defaults to true. If set to false and a result has more pages, you'll find anext()function on the result that you can call to get a promise with the next page of items. This also works for GraphQL queries, as long as your query has a$after: Stringparameter defined, and the results have a single top-level key withpageInfo {hasNextPage, endCursor}and eithernodesoredgeschildren. Useedgeswhen you need per-edge fields such aspermission.boolean: If true, interprets a 404 as false and a 20x as true.metadata: The object on which to set metadata found in the response headers. Defaults toHubkit.ifNotFound: A value to return instead of throwing an exception when the request results in a 404.ifGone: A value to return instead of throwing an exception when the request results in a 410.onError: A function to be called when an error occurs, either in the request itself or an unexpected 4xx or 5xx response. If it's an error response, the error object will havestatus,method,path, andresponseattributes. If the function returnsundefined, the promise will be rejected as usual (or the request retried in some special cases, like network failures and abuse quota 403s), if it returnsHubkit.RETRYthe request will be retried, if it returnsHubkit.DONT_RETRYthe promise will always be rejected, and if returns any other value the promise will be resolved with the returned value. If multiple onError handlers are assigned (e.g., in default options and in per-request options), they will all be executed, and the first non-undefined value from the most specific handler will be used.maxTries: The maximum number of times that a request will be tried (including the original call) ifonErrorkeeps returningHubkit.RETRY.onSend: A function to be called before every individual request gets sent to GitHub. The sole argument will be a string indicating the reason for the request:initialfor the initial request,pagefor an automatic next page request (if theallPagesoption is on), andretryfor an explicit or automatic retry. The function can return a duration in milliseconds that will override the timeout provided in the options (if any). The function can also return a promise for the above, in which case the request will be held until the promise is resolved.onReceive. A function to be called after a reponse (or error) is received from GitHub. If a response was received then the function will be passed an object with propertiesapi(indicating the API used, and hence the quota pool) andcost(how much quota was used by this request). The function's return value, if any, is discarded.gheVersion: A string representing the version of the GitHub Enterprise server you're making calls to. You can retrieve it via a request to/meta. Ignored if your host ishttps://api.github.com(and all#ghepreprocessing directives pass automatically). Otherwise, if a#ghedirective is encountered andgheVersionis not set then an error is thrown.scopes: An array of strings representing all the scopes granted to the user's token. (Note that some scopes imply others, but Hubkit doesn't expand these internally — you might want to do so yourself.) If a#scopedirective is encountered andscopesis not set then an error is thrown.