index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. import isPlainObject from 'is-plain-object';
  2. import { getUserAgent } from 'universal-user-agent';
  3. function lowercaseKeys(object) {
  4. if (!object) {
  5. return {};
  6. }
  7. return Object.keys(object).reduce((newObj, key) => {
  8. newObj[key.toLowerCase()] = object[key];
  9. return newObj;
  10. }, {});
  11. }
  12. function mergeDeep(defaults, options) {
  13. const result = Object.assign({}, defaults);
  14. Object.keys(options).forEach((key) => {
  15. if (isPlainObject(options[key])) {
  16. if (!(key in defaults))
  17. Object.assign(result, { [key]: options[key] });
  18. else
  19. result[key] = mergeDeep(defaults[key], options[key]);
  20. }
  21. else {
  22. Object.assign(result, { [key]: options[key] });
  23. }
  24. });
  25. return result;
  26. }
  27. function merge(defaults, route, options) {
  28. if (typeof route === "string") {
  29. let [method, url] = route.split(" ");
  30. options = Object.assign(url ? { method, url } : { url: method }, options);
  31. }
  32. else {
  33. options = Object.assign({}, route);
  34. }
  35. // lowercase header names before merging with defaults to avoid duplicates
  36. options.headers = lowercaseKeys(options.headers);
  37. const mergedOptions = mergeDeep(defaults || {}, options);
  38. // mediaType.previews arrays are merged, instead of overwritten
  39. if (defaults && defaults.mediaType.previews.length) {
  40. mergedOptions.mediaType.previews = defaults.mediaType.previews
  41. .filter((preview) => !mergedOptions.mediaType.previews.includes(preview))
  42. .concat(mergedOptions.mediaType.previews);
  43. }
  44. mergedOptions.mediaType.previews = mergedOptions.mediaType.previews.map((preview) => preview.replace(/-preview/, ""));
  45. return mergedOptions;
  46. }
  47. function addQueryParameters(url, parameters) {
  48. const separator = /\?/.test(url) ? "&" : "?";
  49. const names = Object.keys(parameters);
  50. if (names.length === 0) {
  51. return url;
  52. }
  53. return (url +
  54. separator +
  55. names
  56. .map((name) => {
  57. if (name === "q") {
  58. return ("q=" + parameters.q.split("+").map(encodeURIComponent).join("+"));
  59. }
  60. return `${name}=${encodeURIComponent(parameters[name])}`;
  61. })
  62. .join("&"));
  63. }
  64. const urlVariableRegex = /\{[^}]+\}/g;
  65. function removeNonChars(variableName) {
  66. return variableName.replace(/^\W+|\W+$/g, "").split(/,/);
  67. }
  68. function extractUrlVariableNames(url) {
  69. const matches = url.match(urlVariableRegex);
  70. if (!matches) {
  71. return [];
  72. }
  73. return matches.map(removeNonChars).reduce((a, b) => a.concat(b), []);
  74. }
  75. function omit(object, keysToOmit) {
  76. return Object.keys(object)
  77. .filter((option) => !keysToOmit.includes(option))
  78. .reduce((obj, key) => {
  79. obj[key] = object[key];
  80. return obj;
  81. }, {});
  82. }
  83. // Based on https://github.com/bramstein/url-template, licensed under BSD
  84. // TODO: create separate package.
  85. //
  86. // Copyright (c) 2012-2014, Bram Stein
  87. // All rights reserved.
  88. // Redistribution and use in source and binary forms, with or without
  89. // modification, are permitted provided that the following conditions
  90. // are met:
  91. // 1. Redistributions of source code must retain the above copyright
  92. // notice, this list of conditions and the following disclaimer.
  93. // 2. Redistributions in binary form must reproduce the above copyright
  94. // notice, this list of conditions and the following disclaimer in the
  95. // documentation and/or other materials provided with the distribution.
  96. // 3. The name of the author may not be used to endorse or promote products
  97. // derived from this software without specific prior written permission.
  98. // THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED
  99. // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
  100. // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
  101. // EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
  102. // INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
  103. // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  104. // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
  105. // OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  106. // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
  107. // EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  108. /* istanbul ignore file */
  109. function encodeReserved(str) {
  110. return str
  111. .split(/(%[0-9A-Fa-f]{2})/g)
  112. .map(function (part) {
  113. if (!/%[0-9A-Fa-f]/.test(part)) {
  114. part = encodeURI(part).replace(/%5B/g, "[").replace(/%5D/g, "]");
  115. }
  116. return part;
  117. })
  118. .join("");
  119. }
  120. function encodeUnreserved(str) {
  121. return encodeURIComponent(str).replace(/[!'()*]/g, function (c) {
  122. return "%" + c.charCodeAt(0).toString(16).toUpperCase();
  123. });
  124. }
  125. function encodeValue(operator, value, key) {
  126. value =
  127. operator === "+" || operator === "#"
  128. ? encodeReserved(value)
  129. : encodeUnreserved(value);
  130. if (key) {
  131. return encodeUnreserved(key) + "=" + value;
  132. }
  133. else {
  134. return value;
  135. }
  136. }
  137. function isDefined(value) {
  138. return value !== undefined && value !== null;
  139. }
  140. function isKeyOperator(operator) {
  141. return operator === ";" || operator === "&" || operator === "?";
  142. }
  143. function getValues(context, operator, key, modifier) {
  144. var value = context[key], result = [];
  145. if (isDefined(value) && value !== "") {
  146. if (typeof value === "string" ||
  147. typeof value === "number" ||
  148. typeof value === "boolean") {
  149. value = value.toString();
  150. if (modifier && modifier !== "*") {
  151. value = value.substring(0, parseInt(modifier, 10));
  152. }
  153. result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : ""));
  154. }
  155. else {
  156. if (modifier === "*") {
  157. if (Array.isArray(value)) {
  158. value.filter(isDefined).forEach(function (value) {
  159. result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : ""));
  160. });
  161. }
  162. else {
  163. Object.keys(value).forEach(function (k) {
  164. if (isDefined(value[k])) {
  165. result.push(encodeValue(operator, value[k], k));
  166. }
  167. });
  168. }
  169. }
  170. else {
  171. const tmp = [];
  172. if (Array.isArray(value)) {
  173. value.filter(isDefined).forEach(function (value) {
  174. tmp.push(encodeValue(operator, value));
  175. });
  176. }
  177. else {
  178. Object.keys(value).forEach(function (k) {
  179. if (isDefined(value[k])) {
  180. tmp.push(encodeUnreserved(k));
  181. tmp.push(encodeValue(operator, value[k].toString()));
  182. }
  183. });
  184. }
  185. if (isKeyOperator(operator)) {
  186. result.push(encodeUnreserved(key) + "=" + tmp.join(","));
  187. }
  188. else if (tmp.length !== 0) {
  189. result.push(tmp.join(","));
  190. }
  191. }
  192. }
  193. }
  194. else {
  195. if (operator === ";") {
  196. if (isDefined(value)) {
  197. result.push(encodeUnreserved(key));
  198. }
  199. }
  200. else if (value === "" && (operator === "&" || operator === "?")) {
  201. result.push(encodeUnreserved(key) + "=");
  202. }
  203. else if (value === "") {
  204. result.push("");
  205. }
  206. }
  207. return result;
  208. }
  209. function parseUrl(template) {
  210. return {
  211. expand: expand.bind(null, template),
  212. };
  213. }
  214. function expand(template, context) {
  215. var operators = ["+", "#", ".", "/", ";", "?", "&"];
  216. return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, function (_, expression, literal) {
  217. if (expression) {
  218. let operator = "";
  219. const values = [];
  220. if (operators.indexOf(expression.charAt(0)) !== -1) {
  221. operator = expression.charAt(0);
  222. expression = expression.substr(1);
  223. }
  224. expression.split(/,/g).forEach(function (variable) {
  225. var tmp = /([^:\*]*)(?::(\d+)|(\*))?/.exec(variable);
  226. values.push(getValues(context, operator, tmp[1], tmp[2] || tmp[3]));
  227. });
  228. if (operator && operator !== "+") {
  229. var separator = ",";
  230. if (operator === "?") {
  231. separator = "&";
  232. }
  233. else if (operator !== "#") {
  234. separator = operator;
  235. }
  236. return (values.length !== 0 ? operator : "") + values.join(separator);
  237. }
  238. else {
  239. return values.join(",");
  240. }
  241. }
  242. else {
  243. return encodeReserved(literal);
  244. }
  245. });
  246. }
  247. function parse(options) {
  248. // https://fetch.spec.whatwg.org/#methods
  249. let method = options.method.toUpperCase();
  250. // replace :varname with {varname} to make it RFC 6570 compatible
  251. let url = (options.url || "/").replace(/:([a-z]\w+)/g, "{+$1}");
  252. let headers = Object.assign({}, options.headers);
  253. let body;
  254. let parameters = omit(options, [
  255. "method",
  256. "baseUrl",
  257. "url",
  258. "headers",
  259. "request",
  260. "mediaType",
  261. ]);
  262. // extract variable names from URL to calculate remaining variables later
  263. const urlVariableNames = extractUrlVariableNames(url);
  264. url = parseUrl(url).expand(parameters);
  265. if (!/^http/.test(url)) {
  266. url = options.baseUrl + url;
  267. }
  268. const omittedParameters = Object.keys(options)
  269. .filter((option) => urlVariableNames.includes(option))
  270. .concat("baseUrl");
  271. const remainingParameters = omit(parameters, omittedParameters);
  272. const isBinaryRequset = /application\/octet-stream/i.test(headers.accept);
  273. if (!isBinaryRequset) {
  274. if (options.mediaType.format) {
  275. // e.g. application/vnd.github.v3+json => application/vnd.github.v3.raw
  276. headers.accept = headers.accept
  277. .split(/,/)
  278. .map((preview) => preview.replace(/application\/vnd(\.\w+)(\.v3)?(\.\w+)?(\+json)?$/, `application/vnd$1$2.${options.mediaType.format}`))
  279. .join(",");
  280. }
  281. if (options.mediaType.previews.length) {
  282. const previewsFromAcceptHeader = headers.accept.match(/[\w-]+(?=-preview)/g) || [];
  283. headers.accept = previewsFromAcceptHeader
  284. .concat(options.mediaType.previews)
  285. .map((preview) => {
  286. const format = options.mediaType.format
  287. ? `.${options.mediaType.format}`
  288. : "+json";
  289. return `application/vnd.github.${preview}-preview${format}`;
  290. })
  291. .join(",");
  292. }
  293. }
  294. // for GET/HEAD requests, set URL query parameters from remaining parameters
  295. // for PATCH/POST/PUT/DELETE requests, set request body from remaining parameters
  296. if (["GET", "HEAD"].includes(method)) {
  297. url = addQueryParameters(url, remainingParameters);
  298. }
  299. else {
  300. if ("data" in remainingParameters) {
  301. body = remainingParameters.data;
  302. }
  303. else {
  304. if (Object.keys(remainingParameters).length) {
  305. body = remainingParameters;
  306. }
  307. else {
  308. headers["content-length"] = 0;
  309. }
  310. }
  311. }
  312. // default content-type for JSON if body is set
  313. if (!headers["content-type"] && typeof body !== "undefined") {
  314. headers["content-type"] = "application/json; charset=utf-8";
  315. }
  316. // GitHub expects 'content-length: 0' header for PUT/PATCH requests without body.
  317. // fetch does not allow to set `content-length` header, but we can set body to an empty string
  318. if (["PATCH", "PUT"].includes(method) && typeof body === "undefined") {
  319. body = "";
  320. }
  321. // Only return body/request keys if present
  322. return Object.assign({ method, url, headers }, typeof body !== "undefined" ? { body } : null, options.request ? { request: options.request } : null);
  323. }
  324. function endpointWithDefaults(defaults, route, options) {
  325. return parse(merge(defaults, route, options));
  326. }
  327. function withDefaults(oldDefaults, newDefaults) {
  328. const DEFAULTS = merge(oldDefaults, newDefaults);
  329. const endpoint = endpointWithDefaults.bind(null, DEFAULTS);
  330. return Object.assign(endpoint, {
  331. DEFAULTS,
  332. defaults: withDefaults.bind(null, DEFAULTS),
  333. merge: merge.bind(null, DEFAULTS),
  334. parse,
  335. });
  336. }
  337. const VERSION = "6.0.1";
  338. const userAgent = `octokit-endpoint.js/${VERSION} ${getUserAgent()}`;
  339. // DEFAULTS has all properties set that EndpointOptions has, except url.
  340. // So we use RequestParameters and add method as additional required property.
  341. const DEFAULTS = {
  342. method: "GET",
  343. baseUrl: "https://api.github.com",
  344. headers: {
  345. accept: "application/vnd.github.v3+json",
  346. "user-agent": userAgent,
  347. },
  348. mediaType: {
  349. format: "",
  350. previews: [],
  351. },
  352. };
  353. const endpoint = withDefaults(null, DEFAULTS);
  354. export { endpoint };
  355. //# sourceMappingURL=index.js.map