Skip to content
This repository was archived by the owner on Oct 10, 2022. It is now read-only.

Commit 4f7ffea

Browse files
committed
Merge pull request #67 from netlify/feature/improve-client-methods
1 parent a8f879b commit 4f7ffea

File tree

10 files changed

+237
-213
lines changed

10 files changed

+237
-213
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
"folder-walker": "^3.2.0",
3535
"from2-array": "0.0.4",
3636
"hasha": "^3.0.0",
37-
"is-stream": "^1.1.0",
3837
"lodash.camelcase": "^4.3.0",
3938
"lodash.flatten": "^4.4.0",
4039
"lodash.get": "^4.4.2",

src/deploy/upload-files.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ const fs = require('fs')
22

33
const pMap = require('p-map')
44
const backoff = require('backoff')
5-
const debug = require('debug')('netlify:deploy')
65

76
module.exports = uploadFiles
87
async function uploadFiles(api, deployId, uploadList, { concurrentUpload, statusCb, maxRetry }) {
@@ -97,9 +96,6 @@ function retryUpload(uploadFn, maxRetry) {
9796
switch (true) {
9897
case e.status >= 400: // observed errors: 408, 401 (4** swallowed), 502
9998
case e.name === 'FetchError': {
100-
debug(`Upload failed... retrying`)
101-
const msg = e.json || e.data
102-
if (msg) debug('%o', msg)
10399
return fibonacciBackoff.backoff()
104100
}
105101
default: {

src/index.js

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ const set = require('lodash.set')
22
const get = require('lodash.get')
33
const dfn = require('@netlify/open-api')
44
const pWaitFor = require('p-wait-for')
5-
const debug = require('debug')('netlify')
65

7-
const { generateOperation } = require('./open-api')
6+
const { addMethods } = require('./methods')
87
const { getOperations } = require('./operations')
98
const deploy = require('./deploy')
109

1110
class NetlifyAPI {
1211
constructor(accessToken, opts) {
12+
addMethods(this)
13+
1314
// variadic arguments
1415
if (typeof accessToken === 'object') {
1516
opts = accessToken
@@ -22,11 +23,11 @@ class NetlifyAPI {
2223
scheme: dfn.schemes[0],
2324
host: dfn.host,
2425
pathPrefix: dfn.basePath,
25-
accessToken
26+
accessToken,
27+
globalParams: {}
2628
},
2729
opts
2830
)
29-
debug('options: %O', opts)
3031

3132
this.defaultHeaders = {
3233
'User-agent': opts.userAgent,
@@ -36,7 +37,7 @@ class NetlifyAPI {
3637
this.scheme = opts.scheme
3738
this.host = opts.host
3839
this.pathPrefix = opts.pathPrefix
39-
this.globalParams = Object.assign({}, opts.globalParams)
40+
this.globalParams = opts.globalParams
4041
this.accessToken = opts.accessToken
4142
}
4243

@@ -57,25 +58,16 @@ class NetlifyAPI {
5758
}
5859

5960
async getAccessToken(ticket, opts) {
60-
opts = Object.assign(
61-
{
62-
poll: 1000,
63-
timeout: 3.6e6
64-
},
65-
opts
66-
)
67-
debug('getAccessToken options: %O', opts)
61+
opts = Object.assign({ poll: 1000, timeout: 3.6e6 }, opts)
6862

6963
const api = this
7064

7165
const { id } = ticket
7266

7367
let authorizedTicket // ticket capture
7468
const checkTicket = async () => {
75-
debug('checking ticket')
7669
const t = await api.showTicket({ ticketId: id })
7770
if (t.authorized) {
78-
debug('received authorized ticket')
7971
authorizedTicket = t
8072
}
8173
return !!t.authorized
@@ -90,13 +82,6 @@ class NetlifyAPI {
9082
const accessTokenResponse = await api.exchangeTicket({ ticketId: authorizedTicket.id })
9183
// See https://open-api.netlify.com/#/default/exchangeTicket for shape
9284
this.accessToken = accessTokenResponse.access_token
93-
debug('access token details: %O', {
94-
id: accessTokenResponse.id,
95-
user_id: accessTokenResponse.id,
96-
user_email: accessTokenResponse.id,
97-
created_at: accessTokenResponse.id
98-
})
99-
10085
return accessTokenResponse.access_token
10186
}
10287

@@ -108,13 +93,6 @@ class NetlifyAPI {
10893
}
10994
}
11095

111-
const operations = getOperations()
112-
operations.forEach(operation => {
113-
// Generate open-api methods
114-
/* {param1, param2, body, ... }, [opts] */
115-
NetlifyAPI.prototype[operation.operationId] = generateOperation(operation)
116-
})
117-
11896
module.exports = NetlifyAPI
11997

120-
module.exports.methods = operations
98+
module.exports.methods = getOperations()

src/index.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ test('Can specify JSON request body as an object', async t => {
187187
t.true(scope.isDone())
188188
})
189189

190-
test.skip('Can specify JSON request body as a function', async t => {
190+
test('Can specify JSON request body as a function', async t => {
191191
const body = { test: 'test' }
192192
const scope = nock(origin)
193193
.post(`${pathPrefix}/accounts`, body, { 'Content-Type': 'application/json' })
@@ -250,7 +250,7 @@ test('Validates required path parameters', async t => {
250250
.reply(200)
251251

252252
const client = getClient()
253-
await t.throwsAsync(client.updateAccount(), 'Missing required param account_id')
253+
await t.throwsAsync(client.updateAccount(), "Missing required path variable 'account_id'")
254254

255255
t.false(scope.isDone())
256256
})
@@ -262,7 +262,7 @@ test('Validates required query parameters', async t => {
262262
.reply(200)
263263

264264
const client = getClient()
265-
await t.throwsAsync(client.addMemberToAccount({ account_slug }), 'Missing required param email')
265+
await t.throwsAsync(client.addMemberToAccount({ account_slug }), "Missing required query variable 'email'")
266266

267267
t.false(scope.isDone())
268268
})

src/methods/body.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Handle request body
2+
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+
}
31+
32+
module.exports = { addBody }

src/methods/index.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
const fetch = require('node-fetch').default || require('node-fetch') // Webpack will sometimes export default exports in different places
2+
3+
const { getOperations } = require('../operations')
4+
5+
const { getUrl } = require('./url.js')
6+
const { addBody } = require('./body.js')
7+
const { shouldRetry, waitForRetry, MAX_RETRY } = require('./retry.js')
8+
const { parseResponse, getFetchError } = require('./response.js')
9+
10+
// For each OpenAPI operation, add a corresponding method.
11+
// The `operationId` is the method name.
12+
const addMethods = function(NetlifyApi) {
13+
const methods = getMethods(NetlifyApi)
14+
Object.assign(NetlifyApi, methods)
15+
}
16+
17+
const getMethods = function(NetlifyApi) {
18+
const operations = getOperations()
19+
const methods = operations.map(method => getMethod(method, NetlifyApi))
20+
return Object.assign({}, ...methods)
21+
}
22+
23+
const getMethod = function(method, NetlifyApi) {
24+
return {
25+
[method.operationId](params, opts) {
26+
return callMethod(method, NetlifyApi, params, opts)
27+
}
28+
}
29+
}
30+
31+
const callMethod = async function(method, NetlifyApi, params, opts) {
32+
const requestParams = Object.assign({}, NetlifyApi.globalParams, params)
33+
const url = getUrl(method, NetlifyApi, requestParams)
34+
const optsA = getOpts(method, NetlifyApi, requestParams, opts)
35+
36+
const response = await makeRequestOrRetry(url, optsA)
37+
38+
const parsedResponse = await parseResponse(response)
39+
return parsedResponse
40+
}
41+
42+
const getOpts = function({ verb, parameters }, NetlifyApi, { body }, opts) {
43+
const optsA = addHttpMethod(verb, opts)
44+
const optsB = addDefaultHeaders(NetlifyApi, optsA)
45+
const optsC = addBody(body, parameters, optsB)
46+
return optsC
47+
}
48+
49+
// Add the HTTP method based on the OpenAPI definition
50+
const addHttpMethod = function(verb, opts) {
51+
return Object.assign({}, opts, { method: verb.toUpperCase() })
52+
}
53+
54+
// Assign default HTTP headers
55+
const addDefaultHeaders = function(NetlifyApi, opts) {
56+
return Object.assign({}, opts, {
57+
headers: Object.assign({}, NetlifyApi.defaultHeaders, opts.headers)
58+
})
59+
}
60+
61+
const makeRequestOrRetry = async function(url, opts) {
62+
for (let index = 0; index <= MAX_RETRY; index++) {
63+
const response = await makeRequest(url, opts)
64+
65+
if (shouldRetry(response, index)) {
66+
await waitForRetry(response)
67+
} else {
68+
return response
69+
}
70+
}
71+
}
72+
73+
const makeRequest = async function(url, opts) {
74+
try {
75+
return await fetch(url, opts)
76+
} catch (error) {
77+
throw getFetchError(error, url, opts)
78+
}
79+
}
80+
81+
module.exports = { addMethods }

src/methods/response.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const { JSONHTTPError, TextHTTPError } = require('micro-api-client')
2+
3+
// Read and parse the HTTP response
4+
const parseResponse = async function(response) {
5+
const { method, ErrorType } = getResponseType(response)
6+
const parsedResponse = await response[method]()
7+
8+
if (!response.ok) {
9+
throw new ErrorType(response, parsedResponse)
10+
}
11+
12+
return parsedResponse
13+
}
14+
15+
const getResponseType = function({ headers }) {
16+
const contentType = headers.get('Content-Type')
17+
18+
if (contentType != null && contentType.includes('json')) {
19+
return { method: 'json', ErrorType: JSONHTTPError }
20+
}
21+
22+
return { method: 'text', ErrorType: TextHTTPError }
23+
}
24+
25+
const getFetchError = function(error, url, opts) {
26+
const data = Object.assign({}, opts)
27+
delete data.Authorization
28+
Object.assign(error, { name: 'FetchError', url, data })
29+
return error
30+
}
31+
32+
module.exports = { parseResponse, getFetchError }

src/methods/retry.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// When the API is rate limiting, the request is retried later
2+
const shouldRetry = function(response, index) {
3+
return response.status === RATE_LIMIT_STATUS && index !== MAX_RETRY
4+
}
5+
6+
const waitForRetry = async function(response) {
7+
const delay = getDelay(response)
8+
await sleep(delay)
9+
}
10+
11+
const getDelay = function({ headers }) {
12+
const rateLimitReset = headers.get(RATE_LIMIT_HEADER)
13+
14+
if (!rateLimitReset) {
15+
return DEFAULT_RETRY_DELAY
16+
}
17+
18+
return Math.max(Number(rateLimitReset) * SECS_TO_MSECS - Date.now(), MIN_RETRY_DELAY)
19+
}
20+
21+
const sleep = function(ms) {
22+
return new Promise(resolve => setTimeout(resolve, ms))
23+
}
24+
25+
const DEFAULT_RETRY_DELAY = 5e3
26+
const MIN_RETRY_DELAY = 1e3
27+
const SECS_TO_MSECS = 1e3
28+
const MAX_RETRY = 10
29+
const RATE_LIMIT_STATUS = 429
30+
const RATE_LIMIT_HEADER = 'X-RateLimit-Reset'
31+
32+
module.exports = { shouldRetry, waitForRetry, MAX_RETRY }

src/methods/url.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const queryString = require('qs')
2+
const camelCase = require('lodash.camelcase')
3+
4+
// Replace path parameters and query parameters in the URI, using the OpenAPI
5+
// definition
6+
const getUrl = function({ path, parameters }, NetlifyApi, requestParams) {
7+
const url = `${NetlifyApi.basePath}${path}`
8+
const urlA = addPathParams(url, parameters, requestParams)
9+
const urlB = addQueryParams(urlA, parameters, requestParams)
10+
return urlB
11+
}
12+
13+
const addPathParams = function(url, parameters, requestParams) {
14+
const pathParams = getRequestParams(parameters.path, requestParams, 'path variable')
15+
return Object.entries(pathParams).reduce(addPathParam, url)
16+
}
17+
18+
const addPathParam = function(url, [name, value]) {
19+
return url.replace(`{${name}}`, value)
20+
}
21+
22+
const addQueryParams = function(url, parameters, requestParams) {
23+
const queryParams = getRequestParams(parameters.query, requestParams, 'query variable')
24+
25+
if (Object.keys(queryParams).length === 0) {
26+
return url
27+
}
28+
29+
return `${url}?${queryString.stringify(queryParams)}`
30+
}
31+
32+
const getRequestParams = function(params, requestParams, name) {
33+
const entries = Object.values(params).map(param => getRequestParam(param, requestParams, name))
34+
return Object.assign({}, ...entries)
35+
}
36+
37+
const getRequestParam = function(param, requestParams, name) {
38+
const value = requestParams[param.name] || requestParams[camelCase(param.name)]
39+
40+
if (value !== undefined) {
41+
return { [param.name]: value }
42+
}
43+
44+
if (param.required) {
45+
throw new Error(`Missing required ${name} '${param.name}'`)
46+
}
47+
}
48+
49+
module.exports = { getUrl }

0 commit comments

Comments
 (0)