@netlify/api 13.3.5
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 +22 -0
- package/README.md +182 -0
- package/lib/index.d.ts +54 -0
- package/lib/index.js +78 -0
- package/lib/methods/body.d.ts +1 -0
- package/lib/methods/body.js +25 -0
- package/lib/methods/index.d.ts +6 -0
- package/lib/methods/index.js +84 -0
- package/lib/methods/params.d.ts +1 -0
- package/lib/methods/params.js +14 -0
- package/lib/methods/response.d.ts +2 -0
- package/lib/methods/response.js +44 -0
- package/lib/methods/retry.d.ts +7 -0
- package/lib/methods/retry.js +40 -0
- package/lib/methods/url.d.ts +4 -0
- package/lib/methods/url.js +24 -0
- package/lib/omit.d.ts +1 -0
- package/lib/omit.js +7 -0
- package/lib/open_api.d.ts +1 -0
- package/lib/open_api.js +5 -0
- package/lib/operations.d.ts +1 -0
- package/lib/operations.js +19 -0
- package/lib/types.d.ts +129 -0
- package/lib/types.js +1 -0
- package/package.json +66 -0
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
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
A Netlify [OpenAPI](https://github.com/netlify/open-api) client that works in the browser and Node.js.
|
|
2
|
+
|
|
3
|
+
## Usage
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { NetlifyAPI } from '@netlify/api'
|
|
7
|
+
|
|
8
|
+
const client = new NetlifyAPI('1234myAccessToken')
|
|
9
|
+
const sites = await client.listSites()
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Using OpenAPI operations
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import { NetlifyAPI } from '@netlify/api'
|
|
16
|
+
|
|
17
|
+
const client = new NetlifyAPI('1234myAccessToken')
|
|
18
|
+
|
|
19
|
+
// Fetch sites
|
|
20
|
+
const sites = await client.listSites()
|
|
21
|
+
|
|
22
|
+
// Create a site. Notice `body` here for sending OpenAPI body
|
|
23
|
+
const site = await client.createSite({
|
|
24
|
+
body: {
|
|
25
|
+
name: `my-awesome-site`,
|
|
26
|
+
// ... https://open-api.netlify.com/#/default/createSite
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// Delete site. Notice `site_id` is a path parameter https://open-api.netlify.com/#/default/deleteSite
|
|
31
|
+
await client.deleteSite({ site_id: siteId })
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## API
|
|
35
|
+
|
|
36
|
+
### `client = new NetlifyAPI([accessToken], [opts])`
|
|
37
|
+
|
|
38
|
+
Create a new instance of the Netlify API client with the provided `accessToken`.
|
|
39
|
+
|
|
40
|
+
`accessToken` is optional. Without it, you can't make authorized requests.
|
|
41
|
+
|
|
42
|
+
`opts` includes:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
const opts = {
|
|
46
|
+
userAgent: 'netlify/js-client',
|
|
47
|
+
scheme: 'https',
|
|
48
|
+
host: 'api.netlify.com',
|
|
49
|
+
pathPrefix: '/api/v1',
|
|
50
|
+
accessToken: '1234myAccessToken',
|
|
51
|
+
agent: undefined, // e.g. HttpsProxyAgent
|
|
52
|
+
globalParams: {}, // parameters you want available for every request.
|
|
53
|
+
// Global params are only sent of the OpenAPI spec specifies the provided params.
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### `client.accessToken`
|
|
58
|
+
|
|
59
|
+
A setter/getter that returns the `accessToken` that the client is configured to use. You can set this after the class is
|
|
60
|
+
instantiated, and all subsequent calls will use the newly set `accessToken`.
|
|
61
|
+
|
|
62
|
+
### `client.basePath`
|
|
63
|
+
|
|
64
|
+
A getter that returns the formatted base URL of the endpoint the client is configured to use.
|
|
65
|
+
|
|
66
|
+
### OpenAPI Client methods
|
|
67
|
+
|
|
68
|
+
The client is dynamically generated from the [OpenAPI](https://github.com/netlify/open-api) definition file. Each method
|
|
69
|
+
is is named after the `operationId` name of each operation. **To see a list of available operations, please see the
|
|
70
|
+
[OpenAPI website](https://open-api.netlify.com/)**.
|
|
71
|
+
|
|
72
|
+
Every OpenAPI operation has the following signature:
|
|
73
|
+
|
|
74
|
+
#### `response = await client.operationId([params], [opts])`
|
|
75
|
+
|
|
76
|
+
Performs a call to the given endpoint corresponding with the `operationId`. Returns a promise resolved with the body of
|
|
77
|
+
the response, or rejected with an error with the details about the request attached. Rejects if the `status` > 400.
|
|
78
|
+
|
|
79
|
+
- `params` is an object that includes any of the required or optional endpoint parameters.
|
|
80
|
+
- `params.body` should be an object which gets serialized to JSON automatically. Any object can live here but refer to
|
|
81
|
+
the OpenAPI specification for allowed fields in a particular request body. It can also be a function returning an
|
|
82
|
+
object.
|
|
83
|
+
- If the endpoint accepts `binary`, `params.body` can be a Node.js readable stream or a function returning one (e.g.
|
|
84
|
+
`() => fs.createReadStream('./foo')`). Using a function is recommended.
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
// example params
|
|
88
|
+
const params = {
|
|
89
|
+
any_param_needed: anyParamNeeded,
|
|
90
|
+
paramsCanAlsoBeCamelCase,
|
|
91
|
+
body: {
|
|
92
|
+
an: 'arbitrary js object',
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Optional `opts` can include any property you want passed to [`node-fetch`](https://github.com/bitinn/node-fetch). The
|
|
98
|
+
`headers` property is merged with some `defaultHeaders`.
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
// example opts
|
|
102
|
+
const opts = {
|
|
103
|
+
headers: {
|
|
104
|
+
// Default headers
|
|
105
|
+
'User-agent': 'netlify-js-client',
|
|
106
|
+
accept: 'application/json',
|
|
107
|
+
},
|
|
108
|
+
// any other properties for node-fetch
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
All operations are conveniently consumed with async/await:
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
try {
|
|
116
|
+
const siteDeploy = await client.getSiteDeploy({
|
|
117
|
+
siteId: '1234abcd',
|
|
118
|
+
deploy_id: '4567',
|
|
119
|
+
})
|
|
120
|
+
// Calls may fail!
|
|
121
|
+
} catch {
|
|
122
|
+
// handle error
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
If the response includes `json` in the `contentType` header, fetch will deserialize the JSON body. Otherwise the `text`
|
|
127
|
+
of the response is returned.
|
|
128
|
+
|
|
129
|
+
### API Flow Methods
|
|
130
|
+
|
|
131
|
+
Some methods have been added in addition to the open API operations that make certain actions simpler to perform.
|
|
132
|
+
|
|
133
|
+
#### `accessToken = await client.getAccessToken(ticket, [opts])`
|
|
134
|
+
|
|
135
|
+
Pass in a [`ticket`](https://open-api.netlify.com/#model-ticket) and get back an `accessToken`. Call this with the
|
|
136
|
+
response from a `client.createTicket({ client_id })` call. Automatically sets the `accessToken` to `this.accessToken`
|
|
137
|
+
and returns `accessToken` for the consumer to save for later.
|
|
138
|
+
|
|
139
|
+
Optional `opts` include:
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
const opts = {
|
|
143
|
+
poll: 1000, // number of ms to wait between polling
|
|
144
|
+
timeout: 3.6e6, // number of ms to wait before timing out
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
See the [authenticating](https://www.netlify.com/docs/api/#authenticating) docs for more context.
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
import open from 'open'
|
|
152
|
+
|
|
153
|
+
const ticket = await client.createTicket({ clientId: CLIENT_ID })
|
|
154
|
+
// Open browser for authentication
|
|
155
|
+
await open(`https://app.netlify.com/authorize?response_type=ticket&ticket=${ticket.id}`)
|
|
156
|
+
|
|
157
|
+
// API is also set up to use the returned access token as a side effect
|
|
158
|
+
// Save this for later so you can quickly set up an authenticated client
|
|
159
|
+
const accessToken = await client.getAccessToken(ticket)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Proxy support
|
|
163
|
+
|
|
164
|
+
**Node.js only**: If this client is used behind a corporate proxy, you can pass an `HttpsProxyAgent` or any other
|
|
165
|
+
`http.Agent` that can handle your situation as `agent` option:
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
import HttpsProxyAgent from 'https-proxy-agent'
|
|
169
|
+
|
|
170
|
+
const proxyUri = 'http(s)://[user:password@]proxyhost:port'
|
|
171
|
+
const agent = new HttpsProxyAgent(proxyUri)
|
|
172
|
+
const client = new NetlifyAPI('1234myAccessToken', { agent })
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Site deployment
|
|
176
|
+
|
|
177
|
+
Support for site deployment has been removed from this package in version 7.0.0. You should consider using the
|
|
178
|
+
[`deploy` command](https://cli.netlify.com/commands/deploy/) of Netlify CLI.
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT. See [LICENSE](./LICENSE) for more details.
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { DynamicMethods } from './types.js';
|
|
2
|
+
type APIOptions = {
|
|
3
|
+
/** @example 'netlify/js-client' */
|
|
4
|
+
userAgent?: string;
|
|
5
|
+
/** @example 'https' */
|
|
6
|
+
scheme?: string;
|
|
7
|
+
/** @example 'api.netlify.com' */
|
|
8
|
+
host?: string;
|
|
9
|
+
/** @example '/api/v1' */
|
|
10
|
+
pathPrefix?: string;
|
|
11
|
+
accessToken?: string;
|
|
12
|
+
/** @example 'HttpsProxyAgent' */
|
|
13
|
+
agent?: string;
|
|
14
|
+
/**
|
|
15
|
+
* parameters you want available for every request.
|
|
16
|
+
* Global params are only sent of the OpenAPI spec specifies the provided params.
|
|
17
|
+
*/
|
|
18
|
+
globalParams?: Record<string, unknown>;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The Netlify API client.
|
|
22
|
+
* @see {@link https://open-api.netlify.com | Open API Reference}
|
|
23
|
+
* @see {@link https://docs.netlify.com/api/get-started | Online Documentation}
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const client = new NetlifyAPI('YOUR_ACCESS_TOKEN')
|
|
28
|
+
* const sites = await client.listSites()
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export interface NetlifyAPI extends DynamicMethods {
|
|
32
|
+
}
|
|
33
|
+
export declare class NetlifyAPI {
|
|
34
|
+
#private;
|
|
35
|
+
defaultHeaders: Record<string, string>;
|
|
36
|
+
/** The protocol is used like `https` */
|
|
37
|
+
scheme: string;
|
|
38
|
+
host: string;
|
|
39
|
+
pathPrefix: string;
|
|
40
|
+
agent?: string;
|
|
41
|
+
globalParams: Record<string, unknown>;
|
|
42
|
+
constructor(options?: APIOptions);
|
|
43
|
+
constructor(accessToken: string | undefined, options?: APIOptions);
|
|
44
|
+
/** Retrieves the access token */
|
|
45
|
+
get accessToken(): string | undefined | null;
|
|
46
|
+
set accessToken(token: string | undefined | null);
|
|
47
|
+
get basePath(): string;
|
|
48
|
+
getAccessToken(ticket: any, { poll, timeout }?: {
|
|
49
|
+
poll?: number | undefined;
|
|
50
|
+
timeout?: number | undefined;
|
|
51
|
+
}): Promise<string | undefined>;
|
|
52
|
+
}
|
|
53
|
+
export declare const methods: any[];
|
|
54
|
+
export {};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
// 1 second
|
|
6
|
+
const DEFAULT_TICKET_POLL = 1e3;
|
|
7
|
+
// 1 hour
|
|
8
|
+
const DEFAULT_TICKET_TIMEOUT = 3.6e6;
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
|
10
|
+
export class NetlifyAPI {
|
|
11
|
+
#accessToken = null;
|
|
12
|
+
defaultHeaders = {
|
|
13
|
+
accept: 'application/json',
|
|
14
|
+
};
|
|
15
|
+
/** The protocol is used like `https` */
|
|
16
|
+
scheme;
|
|
17
|
+
host;
|
|
18
|
+
pathPrefix;
|
|
19
|
+
agent;
|
|
20
|
+
globalParams = {};
|
|
21
|
+
constructor(firstArg, secondArg) {
|
|
22
|
+
// variadic arguments
|
|
23
|
+
const [accessTokenInput, options = {}] = typeof firstArg === 'object' ? [null, firstArg] : [firstArg, secondArg];
|
|
24
|
+
this.globalParams = options.globalParams || {};
|
|
25
|
+
this.agent = options.agent;
|
|
26
|
+
this.scheme = options.scheme || openApiSpec.schemes[0];
|
|
27
|
+
this.host = options.host || openApiSpec.host;
|
|
28
|
+
this.pathPrefix = options.pathPrefix || openApiSpec.basePath;
|
|
29
|
+
// use the setter to set the header as well
|
|
30
|
+
this.accessToken = options.accessToken || accessTokenInput || null;
|
|
31
|
+
this.defaultHeaders['User-agent'] = options.userAgent || 'netlify/js-client';
|
|
32
|
+
const methods = getMethods({
|
|
33
|
+
basePath: this.basePath,
|
|
34
|
+
defaultHeaders: this.defaultHeaders,
|
|
35
|
+
agent: this.agent,
|
|
36
|
+
globalParams: this.globalParams,
|
|
37
|
+
});
|
|
38
|
+
Object.assign(this, { ...methods });
|
|
39
|
+
}
|
|
40
|
+
/** Retrieves the access token */
|
|
41
|
+
get accessToken() {
|
|
42
|
+
return this.#accessToken;
|
|
43
|
+
}
|
|
44
|
+
set accessToken(token) {
|
|
45
|
+
if (!token) {
|
|
46
|
+
delete this.defaultHeaders.Authorization;
|
|
47
|
+
this.#accessToken = null;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.#accessToken = token;
|
|
51
|
+
this.defaultHeaders.Authorization = `Bearer ${this.#accessToken}`;
|
|
52
|
+
}
|
|
53
|
+
get basePath() {
|
|
54
|
+
return `${this.scheme}://${this.host}${this.pathPrefix}`;
|
|
55
|
+
}
|
|
56
|
+
async getAccessToken(ticket, { poll = DEFAULT_TICKET_POLL, timeout = DEFAULT_TICKET_TIMEOUT } = {}) {
|
|
57
|
+
const { id } = ticket;
|
|
58
|
+
// ticket capture
|
|
59
|
+
let authorizedTicket;
|
|
60
|
+
const checkTicket = async () => {
|
|
61
|
+
const t = await this.showTicket({ ticketId: id });
|
|
62
|
+
if (t.authorized) {
|
|
63
|
+
authorizedTicket = t;
|
|
64
|
+
}
|
|
65
|
+
return Boolean(t.authorized);
|
|
66
|
+
};
|
|
67
|
+
await pWaitFor(checkTicket, {
|
|
68
|
+
interval: poll,
|
|
69
|
+
timeout,
|
|
70
|
+
message: 'Timeout while waiting for ticket grant',
|
|
71
|
+
});
|
|
72
|
+
const accessTokenResponse = await this.exchangeTicket({ ticketId: authorizedTicket.id });
|
|
73
|
+
// See https://open-api.netlify.com/#/default/exchangeTicket for shape
|
|
74
|
+
this.accessToken = accessTokenResponse.access_token;
|
|
75
|
+
return accessTokenResponse.access_token;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
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,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,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(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,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,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
|
+
};
|
package/lib/omit.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function omit<T extends Record<string | number | symbol, unknown>, K extends keyof T>(obj: T, keys: K[]): Omit<T, K>;
|
package/lib/omit.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const openApiSpec: any;
|
package/lib/open_api.js
ADDED
|
@@ -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(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/lib/types.d.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { ReadStream } from 'node:fs';
|
|
2
|
+
import type { operations } from '@netlify/open-api';
|
|
3
|
+
import type { RequestInit } from 'node-fetch';
|
|
4
|
+
/**
|
|
5
|
+
* Determines whether all keys in T are optional.
|
|
6
|
+
*/
|
|
7
|
+
type AreAllOptional<T> = keyof T extends never ? true : T extends Record<string, any> ? {
|
|
8
|
+
[K in keyof T]-?: undefined extends T[K] ? never : K;
|
|
9
|
+
}[keyof T] extends never ? true : false : false;
|
|
10
|
+
/**
|
|
11
|
+
* Determines whether `path` and `query` are both optional.
|
|
12
|
+
*/
|
|
13
|
+
type IsPathAndQueryOptional<K extends keyof operations> = 'parameters' extends keyof operations[K] ? AreAllOptional<'path' extends keyof operations[K]['parameters'] ? operations[K]['parameters']['path'] : object> extends true ? AreAllOptional<'query' extends keyof operations[K]['parameters'] ? operations[K]['parameters']['query'] : object> extends true ? true : false : false : true;
|
|
14
|
+
/**
|
|
15
|
+
* Converts snake_case to camelCase for TypeScript types.
|
|
16
|
+
*/
|
|
17
|
+
type CamelCase<S extends string> = S extends `${infer T}_${infer U}` ? `${T}${Capitalize<CamelCase<U>>}` : S;
|
|
18
|
+
/**
|
|
19
|
+
* Creates a union of both snake_case and camelCase keys with their respective types.
|
|
20
|
+
*/
|
|
21
|
+
type SnakeToCamel<T> = {
|
|
22
|
+
[K in keyof T as CamelCase<K & string>]: T[K];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Combines snake_case and camelCase parameters into one Params type.
|
|
26
|
+
*/
|
|
27
|
+
type Params<T> = SnakeToCamel<T> | T;
|
|
28
|
+
type HasRequestBody<K extends keyof operations> = 'requestBody' extends keyof operations[K] ? operations[K]['requestBody'] extends {
|
|
29
|
+
content: any;
|
|
30
|
+
} ? 'application/json' extends keyof operations[K]['requestBody']['content'] ? true : 'application/octet-stream' extends keyof operations[K]['requestBody']['content'] ? true : false : false : false;
|
|
31
|
+
/**
|
|
32
|
+
* Extracts the request body type from the operation.
|
|
33
|
+
*/
|
|
34
|
+
type RequestBody<K extends keyof operations> = 'requestBody' extends keyof operations[K] ? operations[K]['requestBody'] extends {
|
|
35
|
+
content: any;
|
|
36
|
+
} ? 'application/json' extends keyof operations[K]['requestBody']['content'] ? operations[K]['requestBody']['content']['application/json'] : 'application/octet-stream' extends keyof operations[K]['requestBody']['content'] ? ReadStream | (() => ReadStream) : never : never : never;
|
|
37
|
+
type IsRequestBodyJson<K extends keyof operations> = 'requestBody' extends keyof operations[K] ? operations[K]['requestBody'] extends {
|
|
38
|
+
content: any;
|
|
39
|
+
} ? 'application/json' extends keyof operations[K]['requestBody']['content'] ? true : false : false : false;
|
|
40
|
+
type IsRequestBodyOctetStream<K extends keyof operations> = 'requestBody' extends keyof operations[K] ? operations[K]['requestBody'] extends {
|
|
41
|
+
content: any;
|
|
42
|
+
} ? 'application/octet-stream' extends keyof operations[K]['requestBody']['content'] ? true : false : false : false;
|
|
43
|
+
type RequestBodyParam<K extends keyof operations> = HasRequestBody<K> extends true ? IsRequestBodyOptional<K> extends true ? DetailedRequestBodyOptional<K> : DetailedRequestBody<K> : never;
|
|
44
|
+
type DetailedRequestBody<K extends keyof operations> = IsRequestBodyJson<K> extends true ? {
|
|
45
|
+
/**
|
|
46
|
+
* The request body for `application/json`.
|
|
47
|
+
* Automatically serialized to JSON based on the operation.
|
|
48
|
+
* Can be a JSON object or a function returning one.
|
|
49
|
+
*/
|
|
50
|
+
body: RequestBody<K> | (() => RequestBody<K>);
|
|
51
|
+
} : IsRequestBodyOctetStream<K> extends true ? {
|
|
52
|
+
/**
|
|
53
|
+
* The request body for `application/octet-stream`.
|
|
54
|
+
* Can be a Node.js readable stream or a function returning one
|
|
55
|
+
* @example
|
|
56
|
+
* fs.createReadStream('./file')
|
|
57
|
+
* @example
|
|
58
|
+
* () => fs.createReadStream('./file')
|
|
59
|
+
*/
|
|
60
|
+
body: ReadStream | (() => ReadStream);
|
|
61
|
+
} : never;
|
|
62
|
+
type DetailedRequestBodyOptional<K extends keyof operations> = IsRequestBodyJson<K> extends true ? {
|
|
63
|
+
/**
|
|
64
|
+
* The request body for `application/json`.
|
|
65
|
+
* Automatically serialized to JSON based on the operation.
|
|
66
|
+
* Can be a JSON object or a function returning one.
|
|
67
|
+
*/
|
|
68
|
+
body?: RequestBody<K> | (() => RequestBody<K>);
|
|
69
|
+
} : IsRequestBodyOctetStream<K> extends true ? {
|
|
70
|
+
/**
|
|
71
|
+
* The request body for `application/octet-stream`.
|
|
72
|
+
* Can be a Node.js readable stream or a function returning one
|
|
73
|
+
* @example
|
|
74
|
+
* fs.createReadStream('./file')
|
|
75
|
+
* @example
|
|
76
|
+
* () => fs.createReadStream('./file')
|
|
77
|
+
*/
|
|
78
|
+
body?: ReadStream | (() => ReadStream);
|
|
79
|
+
} : never;
|
|
80
|
+
/**
|
|
81
|
+
* Determines whether all properties in the request body are optional.
|
|
82
|
+
*/
|
|
83
|
+
type IsRequestBodyOptional<K extends keyof operations> = HasRequestBody<K> extends true ? (AreAllOptional<RequestBody<K>> extends true ? true : false) : true;
|
|
84
|
+
/**
|
|
85
|
+
* Determines whether any parameters or request body are required.
|
|
86
|
+
*/
|
|
87
|
+
type IsParamsOrRequestBodyRequired<K extends keyof operations> = 'parameters' extends keyof operations[K] ? IsPathAndQueryOptional<K> extends true ? IsRequestBodyOptional<K> extends true ? false : true : true : false;
|
|
88
|
+
/**
|
|
89
|
+
* Extracts and combines `path` and `query` parameters into a single type.
|
|
90
|
+
*/
|
|
91
|
+
type ExtractPathAndQueryParameters<K extends keyof operations> = 'parameters' extends keyof operations[K] ? 'path' extends keyof operations[K]['parameters'] ? 'query' extends keyof operations[K]['parameters'] ? Params<Omit<operations[K]['parameters']['path'], keyof operations[K]['parameters']['query']> & operations[K]['parameters']['query']> : Params<operations[K]['parameters']['path']> : 'query' extends keyof operations[K]['parameters'] ? Params<operations[K]['parameters']['query']> : undefined : undefined;
|
|
92
|
+
/**
|
|
93
|
+
* Combines path, query, and request body parameters into a single type.
|
|
94
|
+
*/
|
|
95
|
+
type CombinedParamsAndRequestBody<K extends keyof operations> = HasRequestBody<K> extends true ? ExtractPathAndQueryParameters<K> & RequestBodyParam<K> : ExtractPathAndQueryParameters<K>;
|
|
96
|
+
type OperationParams<K extends keyof operations> = IsParamsOrRequestBodyRequired<K> extends false ? CombinedParamsAndRequestBody<K> | void | undefined : CombinedParamsAndRequestBody<K>;
|
|
97
|
+
type SuccessHttpStatusCodes = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226;
|
|
98
|
+
/**
|
|
99
|
+
* Extracts the response type from the operation.
|
|
100
|
+
*/
|
|
101
|
+
type OperationResponse<K extends keyof operations> = 'responses' extends keyof operations[K] ? SuccessHttpStatusCodes extends infer StatusKeys ? StatusKeys extends keyof operations[K]['responses'] ? 'content' extends keyof operations[K]['responses'][StatusKeys] ? 'application/json' extends keyof operations[K]['responses'][StatusKeys]['content'] ? operations[K]['responses'][StatusKeys]['content']['application/json'] : never : never : never : never : never;
|
|
102
|
+
export type DynamicMethods = {
|
|
103
|
+
[K in keyof operations]: (params: OperationParams<K>,
|
|
104
|
+
/**
|
|
105
|
+
* Any properties you want passed to `node-fetch`.
|
|
106
|
+
*
|
|
107
|
+
* The `headers` property is merged with some `defaultHeaders`:
|
|
108
|
+
* ```ts
|
|
109
|
+
* {
|
|
110
|
+
* 'User-agent': 'netlify-js-client',
|
|
111
|
+
* 'accept': 'application/json',
|
|
112
|
+
* }
|
|
113
|
+
* ```
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* const site = await client.getSite(
|
|
118
|
+
* { site_id: 'YOUR_SITE_ID' },
|
|
119
|
+
* {
|
|
120
|
+
* headers: {
|
|
121
|
+
* 'X-Example-Header': 'Example Value',
|
|
122
|
+
* },
|
|
123
|
+
* },
|
|
124
|
+
* )
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
opts?: RequestInit | void | undefined) => Promise<OperationResponse<K>>;
|
|
128
|
+
};
|
|
129
|
+
export {};
|
package/lib/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@netlify/api",
|
|
3
|
+
"description": "Netlify Node.js API client",
|
|
4
|
+
"version": "13.3.5",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": "./lib/index.js",
|
|
7
|
+
"main": "./lib/index.js",
|
|
8
|
+
"types": "./lib/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"lib/**"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"prebuild": "rm -rf lib",
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"test": "ava",
|
|
16
|
+
"test:dev": "ava -w",
|
|
17
|
+
"test:ci": "c8 -r lcovonly -r text -r json ava"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Netlify Inc.",
|
|
21
|
+
"contributors": [
|
|
22
|
+
"Mathias Biilmann <matt@netlify.com> (https://twitter.com/biilmann)",
|
|
23
|
+
"David Calavera <david@netlify.com> (https://twitter.com/calavera)",
|
|
24
|
+
"David Wells <david.wells@netlify.com> (https://davidwells.io/)",
|
|
25
|
+
"Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
|
|
26
|
+
"Lukas Holzer <lukas.holzer@netlify.com> (https://twitter.com/luka5c0m)"
|
|
27
|
+
],
|
|
28
|
+
"homepage": "https://github.com/netlify/build",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/netlify/build.git",
|
|
32
|
+
"directory": "packages/js-client"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/netlify/build/issues"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"api client",
|
|
39
|
+
"js client",
|
|
40
|
+
"netlify",
|
|
41
|
+
"node client"
|
|
42
|
+
],
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@netlify/open-api": "^2.37.0",
|
|
45
|
+
"lodash-es": "^4.17.21",
|
|
46
|
+
"micro-api-client": "^3.3.0",
|
|
47
|
+
"node-fetch": "^3.0.0",
|
|
48
|
+
"p-wait-for": "^5.0.0",
|
|
49
|
+
"qs": "^6.9.6"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/lodash-es": "^4.17.6",
|
|
53
|
+
"@types/node": "^14.18.53",
|
|
54
|
+
"ava": "^4.0.0",
|
|
55
|
+
"c8": "^7.11.0",
|
|
56
|
+
"from2-string": "^1.1.0",
|
|
57
|
+
"nock": "^13.0.0",
|
|
58
|
+
"ts-node": "^10.9.1",
|
|
59
|
+
"typescript": "^5.0.0",
|
|
60
|
+
"uuid": "^9.0.0"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": "^14.16.0 || >=16.0.0"
|
|
64
|
+
},
|
|
65
|
+
"gitHead": "27ddc1b91d3d66780166483b42a0f6efddaa14ea"
|
|
66
|
+
}
|