netlify 12.0.0 → 12.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2020 Netlify <team@netlify.com>
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,8 +1,3 @@
1
- ![netlify/js-client](js-client.png)
2
-
3
- [![npm version][npm-img]][npm] [![build status][build-img]][build] [![coverage][coverage-img]][coverage]
4
- [![dependencies][david-img]][david] [![downloads][dl-img]][dl]
5
-
6
1
  A Netlify [OpenAPI](https://github.com/netlify/open-api) client that works in the browser and Node.js.
7
2
 
8
3
  ## Usage
@@ -182,21 +177,6 @@ const client = new NetlifyAPI('1234myAccessToken', { agent })
182
177
  Support for site deployment has been removed from this package in version 7.0.0. You should consider using the
183
178
  [`deploy` command](https://cli.netlify.com/commands/deploy/) of Netlify CLI.
184
179
 
185
- ## Contributing
186
-
187
- See [CONTRIBUTING.md](CONTRIBUTING.md) for more info on how to make contributions to this project.
188
-
189
180
  ## License
190
181
 
191
- MIT. See [LICENSE](LICENSE) for more details.
192
-
193
- [npm-img]: https://img.shields.io/npm/v/netlify.svg
194
- [npm]: https://npmjs.org/package/netlify
195
- [build-img]: https://github.com/netlify/js-client/workflows/Build/badge.svg
196
- [build]: https://github.com/netlify/js-client/actions
197
- [dl-img]: https://img.shields.io/npm/dm/netlify.svg
198
- [dl]: https://npmjs.org/package/netlify
199
- [coverage-img]: https://codecov.io/gh/netlify/js-client/branch/main/graph/badge.svg
200
- [coverage]: https://codecov.io/gh/netlify/js-client
201
- [david-img]: https://david-dm.org/netlify/js-client/status.svg
202
- [david]: https://david-dm.org/netlify/js-client
182
+ MIT. See [LICENSE](./LICENSE) for more details.
package/lib/index.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ export class NetlifyAPI {
2
+ constructor(firstArg: any, secondArg: any);
3
+ set accessToken(arg: string);
4
+ get accessToken(): string;
5
+ get basePath(): string;
6
+ getAccessToken(ticket: any, { poll, timeout }?: {
7
+ poll?: number;
8
+ timeout?: number;
9
+ }): Promise<any>;
10
+ }
11
+ export const methods: any[];
package/lib/index.js ADDED
@@ -0,0 +1,65 @@
1
+ import pWaitFor from 'p-wait-for';
2
+ import { getMethods } from './methods/index.js';
3
+ import { openApiSpec } from './open_api.js';
4
+ import { getOperations } from './operations.js';
5
+ export class NetlifyAPI {
6
+ constructor(firstArg, secondArg) {
7
+ // variadic arguments
8
+ const [accessTokenInput, opts = {}] = typeof firstArg === 'object' ? [null, firstArg] : [firstArg, secondArg];
9
+ // default opts
10
+ const { userAgent = 'netlify/js-client', scheme = openApiSpec.schemes[0], host = openApiSpec.host, pathPrefix = openApiSpec.basePath, accessToken = accessTokenInput, globalParams = {}, agent, } = opts;
11
+ const defaultHeaders = {
12
+ 'User-agent': userAgent,
13
+ accept: 'application/json',
14
+ };
15
+ const basePath = getBasePath({ scheme, host, pathPrefix });
16
+ const methods = getMethods({ basePath, defaultHeaders, agent, globalParams });
17
+ Object.assign(this, { ...methods, defaultHeaders, scheme, host, pathPrefix, globalParams, accessToken, agent });
18
+ }
19
+ get accessToken() {
20
+ const { defaultHeaders: { Authorization }, } = this;
21
+ if (typeof Authorization !== 'string' || !Authorization.startsWith('Bearer ')) {
22
+ return null;
23
+ }
24
+ return Authorization.replace('Bearer ', '');
25
+ }
26
+ set accessToken(token) {
27
+ if (!token) {
28
+ delete this.defaultHeaders.Authorization;
29
+ return;
30
+ }
31
+ this.defaultHeaders.Authorization = `Bearer ${token}`;
32
+ }
33
+ get basePath() {
34
+ return getBasePath({ scheme: this.scheme, host: this.host, pathPrefix: this.pathPrefix });
35
+ }
36
+ async getAccessToken(ticket, { poll = DEFAULT_TICKET_POLL, timeout = DEFAULT_TICKET_TIMEOUT } = {}) {
37
+ const { id } = ticket;
38
+ // ticket capture
39
+ let authorizedTicket;
40
+ const checkTicket = async () => {
41
+ const t = await this.showTicket({ ticketId: id });
42
+ if (t.authorized) {
43
+ authorizedTicket = t;
44
+ }
45
+ return Boolean(t.authorized);
46
+ };
47
+ await pWaitFor(checkTicket, {
48
+ interval: poll,
49
+ timeout,
50
+ message: 'Timeout while waiting for ticket grant',
51
+ });
52
+ const accessTokenResponse = await this.exchangeTicket({ ticketId: authorizedTicket.id });
53
+ // See https://open-api.netlify.com/#/default/exchangeTicket for shape
54
+ this.accessToken = accessTokenResponse.access_token;
55
+ return accessTokenResponse.access_token;
56
+ }
57
+ }
58
+ const getBasePath = function ({ scheme, host, pathPrefix }) {
59
+ return `${scheme}://${host}${pathPrefix}`;
60
+ };
61
+ // 1 second
62
+ const DEFAULT_TICKET_POLL = 1e3;
63
+ // 1 hour
64
+ const DEFAULT_TICKET_TIMEOUT = 3.6e6;
65
+ export const methods = getOperations();
@@ -0,0 +1 @@
1
+ export function addBody(body: any, parameters: any, opts: any): any;
@@ -0,0 +1,25 @@
1
+ // Handle request body
2
+ export const addBody = function (body, parameters, opts) {
3
+ if (!body) {
4
+ return opts;
5
+ }
6
+ const bodyA = typeof body === 'function' ? body() : body;
7
+ if (isBinaryBody(parameters)) {
8
+ return {
9
+ ...opts,
10
+ body: bodyA,
11
+ headers: { 'Content-Type': 'application/octet-stream', ...opts.headers },
12
+ };
13
+ }
14
+ return {
15
+ ...opts,
16
+ body: JSON.stringify(bodyA),
17
+ headers: { 'Content-Type': 'application/json', ...opts.headers },
18
+ };
19
+ };
20
+ const isBinaryBody = function (parameters) {
21
+ return Object.values(parameters.body).some(isBodyParam);
22
+ };
23
+ const isBodyParam = function ({ schema }) {
24
+ return schema && schema.format === 'binary';
25
+ };
@@ -0,0 +1,6 @@
1
+ export function getMethods({ basePath, defaultHeaders, agent, globalParams }: {
2
+ basePath: any;
3
+ defaultHeaders: any;
4
+ agent: any;
5
+ globalParams: any;
6
+ }): any;
@@ -0,0 +1,84 @@
1
+ import fetch from 'node-fetch';
2
+ import { getOperations } from '../operations.js';
3
+ import { addBody } from './body.js';
4
+ import { getRequestParams } from './params.js';
5
+ import { parseResponse, getFetchError } from './response.js';
6
+ import { shouldRetry, waitForRetry, MAX_RETRY } from './retry.js';
7
+ import { getUrl } from './url.js';
8
+ // For each OpenAPI operation, add a corresponding method.
9
+ // The `operationId` is the method name.
10
+ export const getMethods = function ({ basePath, defaultHeaders, agent, globalParams }) {
11
+ const operations = getOperations();
12
+ const methods = operations.map((method) => getMethod({ method, basePath, defaultHeaders, agent, globalParams }));
13
+ return Object.assign({}, ...methods);
14
+ };
15
+ const getMethod = function ({ method, basePath, defaultHeaders, agent, globalParams }) {
16
+ return {
17
+ [method.operationId](params, opts) {
18
+ return callMethod({ method, basePath, defaultHeaders, agent, globalParams, params, opts });
19
+ },
20
+ };
21
+ };
22
+ const callMethod = async function ({ method, basePath, defaultHeaders, agent, globalParams, params, opts }) {
23
+ const requestParams = { ...globalParams, ...params };
24
+ const url = getUrl(method, basePath, requestParams);
25
+ const response = await makeRequestOrRetry({ url, method, defaultHeaders, agent, requestParams, opts });
26
+ const parsedResponse = await parseResponse(response);
27
+ return parsedResponse;
28
+ };
29
+ const getOpts = function ({ method: { verb, parameters }, defaultHeaders, agent, requestParams, opts }) {
30
+ const { body } = requestParams;
31
+ const optsA = addHttpMethod(verb, opts);
32
+ const optsB = addHeaderParams(parameters, requestParams, optsA);
33
+ const optsC = addDefaultHeaders(defaultHeaders, optsB);
34
+ const optsD = addBody(body, parameters, optsC);
35
+ const optsE = addAgent(agent, optsD);
36
+ return optsE;
37
+ };
38
+ // Add header parameters
39
+ const addHeaderParams = function (parameters, requestParams, opts) {
40
+ if (parameters.header === undefined) {
41
+ return opts;
42
+ }
43
+ return { ...opts, headers: getRequestParams(parameters.header, requestParams, 'header parameter') };
44
+ };
45
+ // Add the HTTP method based on the OpenAPI definition
46
+ const addHttpMethod = function (verb, opts) {
47
+ return { ...opts, method: verb.toUpperCase() };
48
+ };
49
+ // Assign default HTTP headers
50
+ const addDefaultHeaders = function (defaultHeaders, opts) {
51
+ return { ...opts, headers: { ...defaultHeaders, ...opts.headers } };
52
+ };
53
+ // Assign fetch agent (like for example HttpsProxyAgent) if there is one
54
+ const addAgent = function (agent, opts) {
55
+ if (agent) {
56
+ return { ...opts, agent };
57
+ }
58
+ return opts;
59
+ };
60
+ const makeRequestOrRetry = async function ({ url, method, defaultHeaders, agent, requestParams, opts }) {
61
+ // Using a loop is simpler here
62
+ for (let index = 0; index <= MAX_RETRY; index++) {
63
+ const optsA = getOpts({ method, defaultHeaders, agent, requestParams, opts });
64
+ const { response, error } = await makeRequest(url, optsA);
65
+ if (shouldRetry({ response, error, method }) && index !== MAX_RETRY) {
66
+ await waitForRetry(response);
67
+ continue;
68
+ }
69
+ if (error !== undefined) {
70
+ throw error;
71
+ }
72
+ return response;
73
+ }
74
+ };
75
+ const makeRequest = async function (url, opts) {
76
+ try {
77
+ const response = await fetch(url, opts);
78
+ return { response };
79
+ }
80
+ catch (error) {
81
+ const errorA = getFetchError(error, url, opts);
82
+ return { error: errorA };
83
+ }
84
+ };
@@ -0,0 +1 @@
1
+ export function getRequestParams(params: any, requestParams: any, name: any): any;
@@ -0,0 +1,14 @@
1
+ import { camelCase } from 'lodash-es';
2
+ export const getRequestParams = function (params, requestParams, name) {
3
+ const entries = Object.values(params).map((param) => getRequestParam(param, requestParams, name));
4
+ return Object.assign({}, ...entries);
5
+ };
6
+ const getRequestParam = function (param, requestParams, name) {
7
+ const value = requestParams[param.name] || requestParams[camelCase(param.name)];
8
+ if (value !== undefined) {
9
+ return { [param.name]: value };
10
+ }
11
+ if (param.required) {
12
+ throw new Error(`Missing required ${name} '${param.name}'`);
13
+ }
14
+ };
@@ -0,0 +1,2 @@
1
+ export function parseResponse(response: any): Promise<any>;
2
+ export function getFetchError(error: any, url: any, opts: any): any;
@@ -0,0 +1,44 @@
1
+ import { JSONHTTPError, TextHTTPError } from 'micro-api-client';
2
+ import omit from 'omit.js';
3
+ // Read and parse the HTTP response
4
+ export const parseResponse = async function (response) {
5
+ const responseType = getResponseType(response);
6
+ const textResponse = await response.text();
7
+ const parsedResponse = parseJsonResponse(response, textResponse, responseType);
8
+ if (!response.ok) {
9
+ const ErrorType = responseType === 'json' ? JSONHTTPError : TextHTTPError;
10
+ throw addFallbackErrorMessage(new ErrorType(response, parsedResponse), textResponse);
11
+ }
12
+ return parsedResponse;
13
+ };
14
+ const getResponseType = function ({ headers }) {
15
+ const contentType = headers.get('Content-Type');
16
+ if (contentType != null && contentType.includes('json')) {
17
+ return 'json';
18
+ }
19
+ return 'text';
20
+ };
21
+ const parseJsonResponse = function (response, textResponse, responseType) {
22
+ if (responseType === 'text') {
23
+ return textResponse;
24
+ }
25
+ try {
26
+ return JSON.parse(textResponse);
27
+ }
28
+ catch {
29
+ throw addFallbackErrorMessage(new TextHTTPError(response, textResponse), textResponse);
30
+ }
31
+ };
32
+ const addFallbackErrorMessage = function (error, textResponse) {
33
+ error.message = error.message || textResponse;
34
+ return error;
35
+ };
36
+ export const getFetchError = function (error, url, opts) {
37
+ const data = omit.default(opts, ['Authorization']);
38
+ if (error.name !== 'FetchError') {
39
+ error.name = 'FetchError';
40
+ }
41
+ error.url = url;
42
+ error.data = data;
43
+ return error;
44
+ };
@@ -0,0 +1,7 @@
1
+ export function shouldRetry({ response, error, method }: {
2
+ response?: {};
3
+ error?: {};
4
+ method?: {};
5
+ }): boolean;
6
+ export function waitForRetry(response: any): Promise<void>;
7
+ export const MAX_RETRY: 5;
@@ -0,0 +1,40 @@
1
+ // We retry:
2
+ // - when receiving a rate limiting response
3
+ // - on network failures due to timeouts
4
+ export const shouldRetry = function ({ response = {}, error = {}, method = {} }) {
5
+ if (response.status === RATE_LIMIT_STATUS || RETRY_ERROR_CODES.has(error.code)) {
6
+ return true;
7
+ }
8
+ // Special case for the `getLatestPluginRuns` endpoint.
9
+ // See https://github.com/netlify/bitballoon/issues/9616.
10
+ if (method.operationId === 'getLatestPluginRuns' && response.status === 500) {
11
+ return true;
12
+ }
13
+ return false;
14
+ };
15
+ export const waitForRetry = async function (response) {
16
+ const delay = getDelay(response);
17
+ await sleep(delay);
18
+ };
19
+ const getDelay = function (response) {
20
+ if (response === undefined) {
21
+ return DEFAULT_RETRY_DELAY;
22
+ }
23
+ const rateLimitReset = response.headers.get(RATE_LIMIT_HEADER);
24
+ if (!rateLimitReset) {
25
+ return DEFAULT_RETRY_DELAY;
26
+ }
27
+ return Math.max(Number(rateLimitReset) * SECS_TO_MSECS - Date.now(), MIN_RETRY_DELAY);
28
+ };
29
+ const sleep = function (ms) {
30
+ return new Promise((resolve) => {
31
+ setTimeout(resolve, ms);
32
+ });
33
+ };
34
+ const DEFAULT_RETRY_DELAY = 5e3;
35
+ const MIN_RETRY_DELAY = 1e3;
36
+ const SECS_TO_MSECS = 1e3;
37
+ export const MAX_RETRY = 5;
38
+ const RATE_LIMIT_STATUS = 429;
39
+ const RATE_LIMIT_HEADER = 'X-RateLimit-Reset';
40
+ const RETRY_ERROR_CODES = new Set(['ETIMEDOUT', 'ECONNRESET']);
@@ -0,0 +1,4 @@
1
+ export function getUrl({ path, parameters }: {
2
+ path: any;
3
+ parameters: any;
4
+ }, basePath: any, requestParams: any): any;
@@ -0,0 +1,24 @@
1
+ import queryString from 'qs';
2
+ import { getRequestParams } from './params.js';
3
+ // Replace path parameters and query parameters in the URI, using the OpenAPI
4
+ // definition
5
+ export const getUrl = function ({ path, parameters }, basePath, requestParams) {
6
+ const url = `${basePath}${path}`;
7
+ const urlA = addPathParams(url, parameters, requestParams);
8
+ const urlB = addQueryParams(urlA, parameters, requestParams);
9
+ return urlB;
10
+ };
11
+ const addPathParams = function (url, parameters, requestParams) {
12
+ const pathParams = getRequestParams(parameters.path, requestParams, 'path variable');
13
+ return Object.entries(pathParams).reduce(addPathParam, url);
14
+ };
15
+ const addPathParam = function (url, [name, value]) {
16
+ return url.replace(`{${name}}`, value);
17
+ };
18
+ const addQueryParams = function (url, parameters, requestParams) {
19
+ const queryParams = getRequestParams(parameters.query, requestParams, 'query variable');
20
+ if (Object.keys(queryParams).length === 0) {
21
+ return url;
22
+ }
23
+ return `${url}?${queryString.stringify(queryParams, { arrayFormat: 'brackets' })}`;
24
+ };
@@ -0,0 +1 @@
1
+ export const openApiSpec: any;
@@ -0,0 +1,5 @@
1
+ import { createRequire } from 'module';
2
+ // TODO: remove once Node.js supports JSON imports with pure ES modules without
3
+ // any experimental flags
4
+ const require = createRequire(import.meta.url);
5
+ export const openApiSpec = require('@netlify/open-api');
@@ -0,0 +1 @@
1
+ export function getOperations(): any[];
@@ -0,0 +1,19 @@
1
+ import omit from 'omit.js';
2
+ import { openApiSpec } from './open_api.js';
3
+ // Retrieve all OpenAPI operations
4
+ export const getOperations = function () {
5
+ return Object.entries(openApiSpec.paths).flatMap(([path, pathItem]) => {
6
+ const operations = omit.default(pathItem, ['parameters']);
7
+ return Object.entries(operations).map(([method, operation]) => {
8
+ const parameters = getParameters(pathItem.parameters, operation.parameters);
9
+ return { ...operation, verb: method, path, parameters };
10
+ });
11
+ });
12
+ };
13
+ const getParameters = function (pathParameters = [], operationParameters = []) {
14
+ const parameters = [...pathParameters, ...operationParameters];
15
+ return parameters.reduce(addParameter, { path: {}, query: {}, body: {} });
16
+ };
17
+ const addParameter = function (parameters, param) {
18
+ return { ...parameters, [param.in]: { ...parameters[param.in], [param.name]: param } };
19
+ };
package/package.json CHANGED
@@ -1,33 +1,19 @@
1
1
  {
2
2
  "name": "netlify",
3
3
  "description": "Netlify Node.js API client",
4
- "version": "12.0.0",
4
+ "version": "12.0.2",
5
5
  "type": "module",
6
+ "exports": "./lib/index.js",
7
+ "main": "./lib/index.js",
8
+ "types": "./lib/index.d.ts",
6
9
  "files": [
7
- "src/**/*.js",
8
- "!src/**/*.test.js"
10
+ "lib/**"
9
11
  ],
10
- "exports": "./src/index.js",
11
- "main": "./src/index.js",
12
12
  "scripts": {
13
- "prepare": "husky install node_modules/@netlify/eslint-config-node/.husky/",
14
- "prepublishOnly": "npm ci && npm test",
15
- "test": "run-s format test:dev",
16
- "format": "run-s format:check-fix:*",
17
- "format:ci": "run-s format:check:*",
18
- "format:check-fix:lint": "run-e format:check:lint format:fix:lint",
19
- "format:check:lint": "cross-env-shell eslint $npm_package_config_eslint",
20
- "format:fix:lint": "cross-env-shell eslint --fix $npm_package_config_eslint",
21
- "format:check-fix:prettier": "run-e format:check:prettier format:fix:prettier",
22
- "format:check:prettier": "cross-env-shell prettier --check $npm_package_config_prettier",
23
- "format:fix:prettier": "cross-env-shell prettier --write $npm_package_config_prettier",
24
- "test:dev": "ava",
25
- "test:ci": "c8 -r lcovonly -r text -r json ava",
26
- "update-snapshots": "ava -u"
27
- },
28
- "config": {
29
- "eslint": "--ignore-path .gitignore --cache --format=codeframe --max-warnings=0 \"{src,tests,.github}/**/*.{cjs,mjs,js,md,html}\" \"*.{cjs,mjs,js,md,html}\" \".*.{cjs,mjs,js,md,html}\"",
30
- "prettier": "--ignore-path .gitignore --loglevel=warn \"{src,tests,.github}/**/*.{cjs,mjs,js,md,yml,json,html}\" \"*.{cjs,mjs,js,yml,json,html}\" \".*.{cjs,mjs,js,yml,json,html}\" \"!package-lock.json\" \"!CHANGELOG.md\""
13
+ "prebuild": "rm -rf lib",
14
+ "build": "tsc",
15
+ "test": "ava",
16
+ "test:ci": "c8 -r lcovonly -r text -r json ava"
31
17
  },
32
18
  "license": "MIT",
33
19
  "author": "Netlify Inc.",
@@ -35,15 +21,16 @@
35
21
  "Mathias Biilmann <matt@netlify.com> (https://twitter.com/biilmann)",
36
22
  "David Calavera <david@netlify.com> (https://twitter.com/calavera)",
37
23
  "David Wells <david.wells@netlify.com> (https://davidwells.io/)",
38
- "Bret Comnes <bcomnes@gmail.com> (https://bret.io)"
24
+ "Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
25
+ "Lukas Holzer <lukas.holzer@netlify.com> (https://twitter.com/luka5c0m)"
39
26
  ],
40
- "homepage": "https://github.com/netlify/js-client",
27
+ "homepage": "https://github.com/netlify/build",
41
28
  "repository": {
42
29
  "type": "git",
43
- "url": "https://github.com/netlify/js-client.git"
30
+ "url": "https://github.com/netlify/build.git"
44
31
  },
45
32
  "bugs": {
46
- "url": "https://github.com/netlify/js-client/issues"
33
+ "url": "https://github.com/netlify/build/issues"
47
34
  },
48
35
  "keywords": [
49
36
  "api client",
@@ -52,8 +39,7 @@
52
39
  "node client"
53
40
  ],
54
41
  "dependencies": {
55
- "@netlify/open-api": "^2.11.0",
56
- "lodash.camelcase": "^4.3.0",
42
+ "@netlify/open-api": "^2.12.0",
57
43
  "micro-api-client": "^3.3.0",
58
44
  "node-fetch": "^3.0.0",
59
45
  "omit.js": "^2.0.2",
@@ -61,23 +47,14 @@
61
47
  "qs": "^6.9.6"
62
48
  },
63
49
  "devDependencies": {
64
- "@netlify/eslint-config-node": "^7.0.0",
65
50
  "ava": "^4.0.0",
66
51
  "c8": "^7.11.0",
67
52
  "from2-string": "^1.1.0",
68
- "husky": "^8.0.0",
69
53
  "nock": "^13.0.0",
70
- "npm-run-all": "^4.1.5",
71
54
  "uuid": "^8.3.2"
72
55
  },
73
56
  "engines": {
74
57
  "node": "^12.20.0 || ^14.14.0 || >=16.0.0"
75
58
  },
76
- "ava": {
77
- "files": [
78
- "src/**/*.test.js"
79
- ],
80
- "verbose": true,
81
- "workerThreads": false
82
- }
59
+ "gitHead": "5555ff52d82f1c26f2074ee52d5e7c5c6c8d02d2"
83
60
  }
package/src/index.js DELETED
@@ -1,92 +0,0 @@
1
- import pWaitFor from 'p-wait-for'
2
-
3
- import { getMethods } from './methods/index.js'
4
- import { openApiSpec } from './open_api.js'
5
- import { getOperations } from './operations.js'
6
-
7
- export class NetlifyAPI {
8
- constructor(firstArg, secondArg) {
9
- // variadic arguments
10
- const [accessTokenInput, opts = {}] = typeof firstArg === 'object' ? [null, firstArg] : [firstArg, secondArg]
11
-
12
- // default opts
13
- const {
14
- userAgent = 'netlify/js-client',
15
- scheme = openApiSpec.schemes[0],
16
- host = openApiSpec.host,
17
- pathPrefix = openApiSpec.basePath,
18
- accessToken = accessTokenInput,
19
- globalParams = {},
20
- agent,
21
- } = opts
22
-
23
- const defaultHeaders = {
24
- 'User-agent': userAgent,
25
- accept: 'application/json',
26
- }
27
-
28
- const basePath = getBasePath({ scheme, host, pathPrefix })
29
- const methods = getMethods({ basePath, defaultHeaders, agent, globalParams })
30
- Object.assign(this, { ...methods, defaultHeaders, scheme, host, pathPrefix, globalParams, accessToken, agent })
31
- }
32
-
33
- get accessToken() {
34
- const {
35
- defaultHeaders: { Authorization },
36
- } = this
37
- if (typeof Authorization !== 'string' || !Authorization.startsWith('Bearer ')) {
38
- return null
39
- }
40
-
41
- return Authorization.replace('Bearer ', '')
42
- }
43
-
44
- set accessToken(token) {
45
- if (!token) {
46
- delete this.defaultHeaders.Authorization
47
- return
48
- }
49
-
50
- this.defaultHeaders.Authorization = `Bearer ${token}`
51
- }
52
-
53
- get basePath() {
54
- return getBasePath({ scheme: this.scheme, host: this.host, pathPrefix: this.pathPrefix })
55
- }
56
-
57
- async getAccessToken(ticket, { poll = DEFAULT_TICKET_POLL, timeout = DEFAULT_TICKET_TIMEOUT } = {}) {
58
- const { id } = ticket
59
-
60
- // ticket capture
61
- let authorizedTicket
62
- const checkTicket = async () => {
63
- const t = await this.showTicket({ ticketId: id })
64
- if (t.authorized) {
65
- authorizedTicket = t
66
- }
67
- return Boolean(t.authorized)
68
- }
69
-
70
- await pWaitFor(checkTicket, {
71
- interval: poll,
72
- timeout,
73
- message: 'Timeout while waiting for ticket grant',
74
- })
75
-
76
- const accessTokenResponse = await this.exchangeTicket({ ticketId: authorizedTicket.id })
77
- // See https://open-api.netlify.com/#/default/exchangeTicket for shape
78
- this.accessToken = accessTokenResponse.access_token
79
- return accessTokenResponse.access_token
80
- }
81
- }
82
-
83
- const getBasePath = function ({ scheme, host, pathPrefix }) {
84
- return `${scheme}://${host}${pathPrefix}`
85
- }
86
-
87
- // 1 second
88
- const DEFAULT_TICKET_POLL = 1e3
89
- // 1 hour
90
- const DEFAULT_TICKET_TIMEOUT = 3.6e6
91
-
92
- export const methods = getOperations()
@@ -1,30 +0,0 @@
1
- // Handle request body
2
- export const addBody = function (body, parameters, opts) {
3
- if (!body) {
4
- return opts
5
- }
6
-
7
- const bodyA = typeof body === 'function' ? body() : body
8
-
9
- if (isBinaryBody(parameters)) {
10
- return {
11
- ...opts,
12
- body: bodyA,
13
- headers: { 'Content-Type': 'application/octet-stream', ...opts.headers },
14
- }
15
- }
16
-
17
- return {
18
- ...opts,
19
- body: JSON.stringify(bodyA),
20
- headers: { 'Content-Type': 'application/json', ...opts.headers },
21
- }
22
- }
23
-
24
- const isBinaryBody = function (parameters) {
25
- return Object.values(parameters.body).some(isBodyParam)
26
- }
27
-
28
- const isBodyParam = function ({ schema }) {
29
- return schema && schema.format === 'binary'
30
- }
@@ -1,100 +0,0 @@
1
- import fetch from 'node-fetch'
2
-
3
- import { getOperations } from '../operations.js'
4
-
5
- import { addBody } from './body.js'
6
- import { getRequestParams } from './params.js'
7
- import { parseResponse, getFetchError } from './response.js'
8
- import { shouldRetry, waitForRetry, MAX_RETRY } from './retry.js'
9
- import { getUrl } from './url.js'
10
-
11
- // For each OpenAPI operation, add a corresponding method.
12
- // The `operationId` is the method name.
13
- export const getMethods = function ({ basePath, defaultHeaders, agent, globalParams }) {
14
- const operations = getOperations()
15
- const methods = operations.map((method) => getMethod({ method, basePath, defaultHeaders, agent, globalParams }))
16
- return Object.assign({}, ...methods)
17
- }
18
-
19
- const getMethod = function ({ method, basePath, defaultHeaders, agent, globalParams }) {
20
- return {
21
- [method.operationId](params, opts) {
22
- return callMethod({ method, basePath, defaultHeaders, agent, globalParams, params, opts })
23
- },
24
- }
25
- }
26
-
27
- const callMethod = async function ({ method, basePath, defaultHeaders, agent, globalParams, params, opts }) {
28
- const requestParams = { ...globalParams, ...params }
29
- const url = getUrl(method, basePath, requestParams)
30
- const response = await makeRequestOrRetry({ url, method, defaultHeaders, agent, requestParams, opts })
31
-
32
- const parsedResponse = await parseResponse(response)
33
- return parsedResponse
34
- }
35
-
36
- const getOpts = function ({ method: { verb, parameters }, defaultHeaders, agent, requestParams, opts }) {
37
- const { body } = requestParams
38
- const optsA = addHttpMethod(verb, opts)
39
- const optsB = addHeaderParams(parameters, requestParams, optsA)
40
- const optsC = addDefaultHeaders(defaultHeaders, optsB)
41
- const optsD = addBody(body, parameters, optsC)
42
- const optsE = addAgent(agent, optsD)
43
- return optsE
44
- }
45
-
46
- // Add header parameters
47
- const addHeaderParams = function (parameters, requestParams, opts) {
48
- if (parameters.header === undefined) {
49
- return opts
50
- }
51
-
52
- return { ...opts, headers: getRequestParams(parameters.header, requestParams, 'header parameter') }
53
- }
54
-
55
- // Add the HTTP method based on the OpenAPI definition
56
- const addHttpMethod = function (verb, opts) {
57
- return { ...opts, method: verb.toUpperCase() }
58
- }
59
-
60
- // Assign default HTTP headers
61
- const addDefaultHeaders = function (defaultHeaders, opts) {
62
- return { ...opts, headers: { ...defaultHeaders, ...opts.headers } }
63
- }
64
-
65
- // Assign fetch agent (like for example HttpsProxyAgent) if there is one
66
- const addAgent = function (agent, opts) {
67
- if (agent) {
68
- return { ...opts, agent }
69
- }
70
- return opts
71
- }
72
-
73
- const makeRequestOrRetry = async function ({ url, method, defaultHeaders, agent, requestParams, opts }) {
74
- // Using a loop is simpler here
75
- for (let index = 0; index <= MAX_RETRY; index++) {
76
- const optsA = getOpts({ method, defaultHeaders, agent, requestParams, opts })
77
- const { response, error } = await makeRequest(url, optsA)
78
-
79
- if (shouldRetry({ response, error, method }) && index !== MAX_RETRY) {
80
- await waitForRetry(response)
81
- continue
82
- }
83
-
84
- if (error !== undefined) {
85
- throw error
86
- }
87
-
88
- return response
89
- }
90
- }
91
-
92
- const makeRequest = async function (url, opts) {
93
- try {
94
- const response = await fetch(url, opts)
95
- return { response }
96
- } catch (error) {
97
- const errorA = getFetchError(error, url, opts)
98
- return { error: errorA }
99
- }
100
- }
@@ -1,18 +0,0 @@
1
- import camelCase from 'lodash.camelcase'
2
-
3
- export const getRequestParams = function (params, requestParams, name) {
4
- const entries = Object.values(params).map((param) => getRequestParam(param, requestParams, name))
5
- return Object.assign({}, ...entries)
6
- }
7
-
8
- const getRequestParam = function (param, requestParams, name) {
9
- const value = requestParams[param.name] || requestParams[camelCase(param.name)]
10
-
11
- if (value !== undefined) {
12
- return { [param.name]: value }
13
- }
14
-
15
- if (param.required) {
16
- throw new Error(`Missing required ${name} '${param.name}'`)
17
- }
18
- }
@@ -1,54 +0,0 @@
1
- import { JSONHTTPError, TextHTTPError } from 'micro-api-client'
2
- import omit from 'omit.js'
3
-
4
- // Read and parse the HTTP response
5
- export const parseResponse = async function (response) {
6
- const responseType = getResponseType(response)
7
- const textResponse = await response.text()
8
-
9
- const parsedResponse = parseJsonResponse(response, textResponse, responseType)
10
-
11
- if (!response.ok) {
12
- const ErrorType = responseType === 'json' ? JSONHTTPError : TextHTTPError
13
- throw addFallbackErrorMessage(new ErrorType(response, parsedResponse), textResponse)
14
- }
15
-
16
- return parsedResponse
17
- }
18
-
19
- const getResponseType = function ({ headers }) {
20
- const contentType = headers.get('Content-Type')
21
-
22
- if (contentType != null && contentType.includes('json')) {
23
- return 'json'
24
- }
25
-
26
- return 'text'
27
- }
28
-
29
- const parseJsonResponse = function (response, textResponse, responseType) {
30
- if (responseType === 'text') {
31
- return textResponse
32
- }
33
-
34
- try {
35
- return JSON.parse(textResponse)
36
- } catch {
37
- throw addFallbackErrorMessage(new TextHTTPError(response, textResponse), textResponse)
38
- }
39
- }
40
-
41
- const addFallbackErrorMessage = function (error, textResponse) {
42
- error.message = error.message || textResponse
43
- return error
44
- }
45
-
46
- export const getFetchError = function (error, url, opts) {
47
- const data = omit.default(opts, ['Authorization'])
48
- if (error.name !== 'FetchError') {
49
- error.name = 'FetchError'
50
- }
51
- error.url = url
52
- error.data = data
53
- return error
54
- }
@@ -1,49 +0,0 @@
1
- // We retry:
2
- // - when receiving a rate limiting response
3
- // - on network failures due to timeouts
4
- export const shouldRetry = function ({ response = {}, error = {}, method = {} }) {
5
- if (response.status === RATE_LIMIT_STATUS || RETRY_ERROR_CODES.has(error.code)) {
6
- return true
7
- }
8
-
9
- // Special case for the `getLatestPluginRuns` endpoint.
10
- // See https://github.com/netlify/bitballoon/issues/9616.
11
- if (method.operationId === 'getLatestPluginRuns' && response.status === 500) {
12
- return true
13
- }
14
-
15
- return false
16
- }
17
-
18
- export const waitForRetry = async function (response) {
19
- const delay = getDelay(response)
20
- await sleep(delay)
21
- }
22
-
23
- const getDelay = function (response) {
24
- if (response === undefined) {
25
- return DEFAULT_RETRY_DELAY
26
- }
27
-
28
- const rateLimitReset = response.headers.get(RATE_LIMIT_HEADER)
29
-
30
- if (!rateLimitReset) {
31
- return DEFAULT_RETRY_DELAY
32
- }
33
-
34
- return Math.max(Number(rateLimitReset) * SECS_TO_MSECS - Date.now(), MIN_RETRY_DELAY)
35
- }
36
-
37
- const sleep = function (ms) {
38
- return new Promise((resolve) => {
39
- setTimeout(resolve, ms)
40
- })
41
- }
42
-
43
- const DEFAULT_RETRY_DELAY = 5e3
44
- const MIN_RETRY_DELAY = 1e3
45
- const SECS_TO_MSECS = 1e3
46
- export const MAX_RETRY = 5
47
- const RATE_LIMIT_STATUS = 429
48
- const RATE_LIMIT_HEADER = 'X-RateLimit-Reset'
49
- const RETRY_ERROR_CODES = new Set(['ETIMEDOUT', 'ECONNRESET'])
@@ -1,31 +0,0 @@
1
- import queryString from 'qs'
2
-
3
- import { getRequestParams } from './params.js'
4
-
5
- // Replace path parameters and query parameters in the URI, using the OpenAPI
6
- // definition
7
- export const getUrl = function ({ path, parameters }, basePath, requestParams) {
8
- const url = `${basePath}${path}`
9
- const urlA = addPathParams(url, parameters, requestParams)
10
- const urlB = addQueryParams(urlA, parameters, requestParams)
11
- return urlB
12
- }
13
-
14
- const addPathParams = function (url, parameters, requestParams) {
15
- const pathParams = getRequestParams(parameters.path, requestParams, 'path variable')
16
- return Object.entries(pathParams).reduce(addPathParam, url)
17
- }
18
-
19
- const addPathParam = function (url, [name, value]) {
20
- return url.replace(`{${name}}`, value)
21
- }
22
-
23
- const addQueryParams = function (url, parameters, requestParams) {
24
- const queryParams = getRequestParams(parameters.query, requestParams, 'query variable')
25
-
26
- if (Object.keys(queryParams).length === 0) {
27
- return url
28
- }
29
-
30
- return `${url}?${queryString.stringify(queryParams, { arrayFormat: 'brackets' })}`
31
- }
package/src/open_api.js DELETED
@@ -1,7 +0,0 @@
1
- import { createRequire } from 'module'
2
-
3
- // TODO: remove once Node.js supports JSON imports with pure ES modules without
4
- // any experimental flags
5
- const require = createRequire(import.meta.url)
6
-
7
- export const openApiSpec = require('@netlify/open-api')
package/src/operations.js DELETED
@@ -1,23 +0,0 @@
1
- import omit from 'omit.js'
2
-
3
- import { openApiSpec } from './open_api.js'
4
-
5
- // Retrieve all OpenAPI operations
6
- export const getOperations = function () {
7
- return Object.entries(openApiSpec.paths).flatMap(([path, pathItem]) => {
8
- const operations = omit.default(pathItem, ['parameters'])
9
- return Object.entries(operations).map(([method, operation]) => {
10
- const parameters = getParameters(pathItem.parameters, operation.parameters)
11
- return { ...operation, verb: method, path, parameters }
12
- })
13
- })
14
- }
15
-
16
- const getParameters = function (pathParameters = [], operationParameters = []) {
17
- const parameters = [...pathParameters, ...operationParameters]
18
- return parameters.reduce(addParameter, { path: {}, query: {}, body: {} })
19
- }
20
-
21
- const addParameter = function (parameters, param) {
22
- return { ...parameters, [param.in]: { ...parameters[param.in], [param.name]: param } }
23
- }