378 lines
13 KiB
JavaScript
378 lines
13 KiB
JavaScript
import isPlainObject from 'is-plain-object';
|
|
import { getUserAgent } from 'universal-user-agent';
|
|
|
|
function lowercaseKeys(object) {
|
|
if (!object) {
|
|
return {};
|
|
}
|
|
return Object.keys(object).reduce((newObj, key) => {
|
|
newObj[key.toLowerCase()] = object[key];
|
|
return newObj;
|
|
}, {});
|
|
}
|
|
|
|
function mergeDeep(defaults, options) {
|
|
const result = Object.assign({}, defaults);
|
|
Object.keys(options).forEach(key => {
|
|
if (isPlainObject(options[key])) {
|
|
if (!(key in defaults))
|
|
Object.assign(result, { [key]: options[key] });
|
|
else
|
|
result[key] = mergeDeep(defaults[key], options[key]);
|
|
}
|
|
else {
|
|
Object.assign(result, { [key]: options[key] });
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function merge(defaults, route, options) {
|
|
if (typeof route === "string") {
|
|
let [method, url] = route.split(" ");
|
|
options = Object.assign(url ? { method, url } : { url: method }, options);
|
|
}
|
|
else {
|
|
options = route || {};
|
|
}
|
|
// lowercase header names before merging with defaults to avoid duplicates
|
|
options.headers = lowercaseKeys(options.headers);
|
|
const mergedOptions = mergeDeep(defaults || {}, options);
|
|
// mediaType.previews arrays are merged, instead of overwritten
|
|
if (defaults && defaults.mediaType.previews.length) {
|
|
mergedOptions.mediaType.previews = defaults.mediaType.previews
|
|
.filter(preview => !mergedOptions.mediaType.previews.includes(preview))
|
|
.concat(mergedOptions.mediaType.previews);
|
|
}
|
|
mergedOptions.mediaType.previews = mergedOptions.mediaType.previews.map((preview) => preview.replace(/-preview/, ""));
|
|
return mergedOptions;
|
|
}
|
|
|
|
function addQueryParameters(url, parameters) {
|
|
const separator = /\?/.test(url) ? "&" : "?";
|
|
const names = Object.keys(parameters);
|
|
if (names.length === 0) {
|
|
return url;
|
|
}
|
|
return (url +
|
|
separator +
|
|
names
|
|
.map(name => {
|
|
if (name === "q") {
|
|
return ("q=" +
|
|
parameters
|
|
.q.split("+")
|
|
.map(encodeURIComponent)
|
|
.join("+"));
|
|
}
|
|
return `${name}=${encodeURIComponent(parameters[name])}`;
|
|
})
|
|
.join("&"));
|
|
}
|
|
|
|
const urlVariableRegex = /\{[^}]+\}/g;
|
|
function removeNonChars(variableName) {
|
|
return variableName.replace(/^\W+|\W+$/g, "").split(/,/);
|
|
}
|
|
function extractUrlVariableNames(url) {
|
|
const matches = url.match(urlVariableRegex);
|
|
if (!matches) {
|
|
return [];
|
|
}
|
|
return matches.map(removeNonChars).reduce((a, b) => a.concat(b), []);
|
|
}
|
|
|
|
function omit(object, keysToOmit) {
|
|
return Object.keys(object)
|
|
.filter(option => !keysToOmit.includes(option))
|
|
.reduce((obj, key) => {
|
|
obj[key] = object[key];
|
|
return obj;
|
|
}, {});
|
|
}
|
|
|
|
// Based on https://github.com/bramstein/url-template, licensed under BSD
|
|
// TODO: create separate package.
|
|
//
|
|
// Copyright (c) 2012-2014, Bram Stein
|
|
// All rights reserved.
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions
|
|
// are met:
|
|
// 1. Redistributions of source code must retain the above copyright
|
|
// notice, this list of conditions and the following disclaimer.
|
|
// 2. Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
// 3. The name of the author may not be used to endorse or promote products
|
|
// derived from this software without specific prior written permission.
|
|
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED
|
|
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
|
// EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
|
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
|
// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
|
// EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
/* istanbul ignore file */
|
|
function encodeReserved(str) {
|
|
return str
|
|
.split(/(%[0-9A-Fa-f]{2})/g)
|
|
.map(function (part) {
|
|
if (!/%[0-9A-Fa-f]/.test(part)) {
|
|
part = encodeURI(part)
|
|
.replace(/%5B/g, "[")
|
|
.replace(/%5D/g, "]");
|
|
}
|
|
return part;
|
|
})
|
|
.join("");
|
|
}
|
|
function encodeUnreserved(str) {
|
|
return encodeURIComponent(str).replace(/[!'()*]/g, function (c) {
|
|
return ("%" +
|
|
c
|
|
.charCodeAt(0)
|
|
.toString(16)
|
|
.toUpperCase());
|
|
});
|
|
}
|
|
function encodeValue(operator, value, key) {
|
|
value =
|
|
operator === "+" || operator === "#"
|
|
? encodeReserved(value)
|
|
: encodeUnreserved(value);
|
|
if (key) {
|
|
return encodeUnreserved(key) + "=" + value;
|
|
}
|
|
else {
|
|
return value;
|
|
}
|
|
}
|
|
function isDefined(value) {
|
|
return value !== undefined && value !== null;
|
|
}
|
|
function isKeyOperator(operator) {
|
|
return operator === ";" || operator === "&" || operator === "?";
|
|
}
|
|
function getValues(context, operator, key, modifier) {
|
|
var value = context[key], result = [];
|
|
if (isDefined(value) && value !== "") {
|
|
if (typeof value === "string" ||
|
|
typeof value === "number" ||
|
|
typeof value === "boolean") {
|
|
value = value.toString();
|
|
if (modifier && modifier !== "*") {
|
|
value = value.substring(0, parseInt(modifier, 10));
|
|
}
|
|
result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : ""));
|
|
}
|
|
else {
|
|
if (modifier === "*") {
|
|
if (Array.isArray(value)) {
|
|
value.filter(isDefined).forEach(function (value) {
|
|
result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : ""));
|
|
});
|
|
}
|
|
else {
|
|
Object.keys(value).forEach(function (k) {
|
|
if (isDefined(value[k])) {
|
|
result.push(encodeValue(operator, value[k], k));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
const tmp = [];
|
|
if (Array.isArray(value)) {
|
|
value.filter(isDefined).forEach(function (value) {
|
|
tmp.push(encodeValue(operator, value));
|
|
});
|
|
}
|
|
else {
|
|
Object.keys(value).forEach(function (k) {
|
|
if (isDefined(value[k])) {
|
|
tmp.push(encodeUnreserved(k));
|
|
tmp.push(encodeValue(operator, value[k].toString()));
|
|
}
|
|
});
|
|
}
|
|
if (isKeyOperator(operator)) {
|
|
result.push(encodeUnreserved(key) + "=" + tmp.join(","));
|
|
}
|
|
else if (tmp.length !== 0) {
|
|
result.push(tmp.join(","));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
if (operator === ";") {
|
|
if (isDefined(value)) {
|
|
result.push(encodeUnreserved(key));
|
|
}
|
|
}
|
|
else if (value === "" && (operator === "&" || operator === "?")) {
|
|
result.push(encodeUnreserved(key) + "=");
|
|
}
|
|
else if (value === "") {
|
|
result.push("");
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
function parseUrl(template) {
|
|
return {
|
|
expand: expand.bind(null, template)
|
|
};
|
|
}
|
|
function expand(template, context) {
|
|
var operators = ["+", "#", ".", "/", ";", "?", "&"];
|
|
return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, function (_, expression, literal) {
|
|
if (expression) {
|
|
let operator = "";
|
|
const values = [];
|
|
if (operators.indexOf(expression.charAt(0)) !== -1) {
|
|
operator = expression.charAt(0);
|
|
expression = expression.substr(1);
|
|
}
|
|
expression.split(/,/g).forEach(function (variable) {
|
|
var tmp = /([^:\*]*)(?::(\d+)|(\*))?/.exec(variable);
|
|
values.push(getValues(context, operator, tmp[1], tmp[2] || tmp[3]));
|
|
});
|
|
if (operator && operator !== "+") {
|
|
var separator = ",";
|
|
if (operator === "?") {
|
|
separator = "&";
|
|
}
|
|
else if (operator !== "#") {
|
|
separator = operator;
|
|
}
|
|
return (values.length !== 0 ? operator : "") + values.join(separator);
|
|
}
|
|
else {
|
|
return values.join(",");
|
|
}
|
|
}
|
|
else {
|
|
return encodeReserved(literal);
|
|
}
|
|
});
|
|
}
|
|
|
|
function parse(options) {
|
|
// https://fetch.spec.whatwg.org/#methods
|
|
let method = options.method.toUpperCase();
|
|
// replace :varname with {varname} to make it RFC 6570 compatible
|
|
let url = options.url.replace(/:([a-z]\w+)/g, "{+$1}");
|
|
let headers = Object.assign({}, options.headers);
|
|
let body;
|
|
let parameters = omit(options, [
|
|
"method",
|
|
"baseUrl",
|
|
"url",
|
|
"headers",
|
|
"request",
|
|
"mediaType"
|
|
]);
|
|
// extract variable names from URL to calculate remaining variables later
|
|
const urlVariableNames = extractUrlVariableNames(url);
|
|
url = parseUrl(url).expand(parameters);
|
|
if (!/^http/.test(url)) {
|
|
url = options.baseUrl + url;
|
|
}
|
|
const omittedParameters = Object.keys(options)
|
|
.filter(option => urlVariableNames.includes(option))
|
|
.concat("baseUrl");
|
|
const remainingParameters = omit(parameters, omittedParameters);
|
|
const isBinaryRequset = /application\/octet-stream/i.test(headers.accept);
|
|
if (!isBinaryRequset) {
|
|
if (options.mediaType.format) {
|
|
// e.g. application/vnd.github.v3+json => application/vnd.github.v3.raw
|
|
headers.accept = headers.accept
|
|
.split(/,/)
|
|
.map(preview => preview.replace(/application\/vnd(\.\w+)(\.v3)?(\.\w+)?(\+json)?$/, `application/vnd$1$2.${options.mediaType.format}`))
|
|
.join(",");
|
|
}
|
|
if (options.mediaType.previews.length) {
|
|
const previewsFromAcceptHeader = headers.accept.match(/[\w-]+(?=-preview)/g) || [];
|
|
headers.accept = previewsFromAcceptHeader
|
|
.concat(options.mediaType.previews)
|
|
.map(preview => {
|
|
const format = options.mediaType.format
|
|
? `.${options.mediaType.format}`
|
|
: "+json";
|
|
return `application/vnd.github.${preview}-preview${format}`;
|
|
})
|
|
.join(",");
|
|
}
|
|
}
|
|
// for GET/HEAD requests, set URL query parameters from remaining parameters
|
|
// for PATCH/POST/PUT/DELETE requests, set request body from remaining parameters
|
|
if (["GET", "HEAD"].includes(method)) {
|
|
url = addQueryParameters(url, remainingParameters);
|
|
}
|
|
else {
|
|
if ("data" in remainingParameters) {
|
|
body = remainingParameters.data;
|
|
}
|
|
else {
|
|
if (Object.keys(remainingParameters).length) {
|
|
body = remainingParameters;
|
|
}
|
|
else {
|
|
headers["content-length"] = 0;
|
|
}
|
|
}
|
|
}
|
|
// default content-type for JSON if body is set
|
|
if (!headers["content-type"] && typeof body !== "undefined") {
|
|
headers["content-type"] = "application/json; charset=utf-8";
|
|
}
|
|
// GitHub expects 'content-length: 0' header for PUT/PATCH requests without body.
|
|
// fetch does not allow to set `content-length` header, but we can set body to an empty string
|
|
if (["PATCH", "PUT"].includes(method) && typeof body === "undefined") {
|
|
body = "";
|
|
}
|
|
// Only return body/request keys if present
|
|
return Object.assign({ method, url, headers }, typeof body !== "undefined" ? { body } : null, options.request ? { request: options.request } : null);
|
|
}
|
|
|
|
function endpointWithDefaults(defaults, route, options) {
|
|
return parse(merge(defaults, route, options));
|
|
}
|
|
|
|
function withDefaults(oldDefaults, newDefaults) {
|
|
const DEFAULTS = merge(oldDefaults, newDefaults);
|
|
const endpoint = endpointWithDefaults.bind(null, DEFAULTS);
|
|
return Object.assign(endpoint, {
|
|
DEFAULTS,
|
|
defaults: withDefaults.bind(null, DEFAULTS),
|
|
merge: merge.bind(null, DEFAULTS),
|
|
parse
|
|
});
|
|
}
|
|
|
|
const VERSION = "0.0.0-development";
|
|
|
|
const userAgent = `octokit-endpoint.js/${VERSION} ${getUserAgent()}`;
|
|
const DEFAULTS = {
|
|
method: "GET",
|
|
baseUrl: "https://api.github.com",
|
|
headers: {
|
|
accept: "application/vnd.github.v3+json",
|
|
"user-agent": userAgent
|
|
},
|
|
mediaType: {
|
|
format: "",
|
|
previews: []
|
|
}
|
|
};
|
|
|
|
const endpoint = withDefaults(null, DEFAULTS);
|
|
|
|
export { endpoint };
|
|
//# sourceMappingURL=index.js.map
|