amazon-sp-api
Advanced tools
Comparing version 0.1.1 to 0.2.0
@@ -7,3 +7,3 @@ const https = require('https'); | ||
let options = { | ||
method:req_options.method || 'GET', | ||
method:req_options.method, | ||
port:443, | ||
@@ -15,5 +15,5 @@ hostname:url.hostname, | ||
let post_params; | ||
if (['post','put'].includes(req_options.method.toLowerCase()) && req_options.query){ | ||
post_params = req_options.query; | ||
options.headers['Content-Length'] = post_params.length; | ||
if (req_options.body){ | ||
post_params = req_options.body; | ||
options.headers['Content-Length'] = Buffer.byteLength(post_params); | ||
} | ||
@@ -26,3 +26,6 @@ let req = https.request(options, (res) => { | ||
res.on('end', () => { | ||
resolve(body); | ||
resolve({ | ||
body:body, | ||
statusCode:res.statusCode | ||
}); | ||
}); | ||
@@ -35,3 +38,3 @@ }); | ||
if (post_params){ | ||
req.write(post_params); | ||
req.write(post_params, 'utf8'); | ||
} | ||
@@ -38,0 +41,0 @@ req.end(); |
@@ -6,2 +6,3 @@ const CustomError = require('./CustomError'); | ||
const credentials = require('./credentials'); | ||
const operations = require('./operations'); | ||
@@ -27,2 +28,3 @@ // Provide credentials as environment variables OR create a path and file ~/.amzspapi/credentials (located in your user folder) | ||
// auto_request_tokens:true // Optional: Whether or not the client should retrieve new access and role credentials if non given or expired. Default is true | ||
// auto_request_throttled:true // Optional: Whether or not the client should automatically retry a request when throttled. Default is true | ||
// } | ||
@@ -47,3 +49,4 @@ constructor(config){ | ||
this._options = Object.assign({ | ||
auto_request_tokens:true | ||
auto_request_tokens:true, | ||
auto_request_throttled:true | ||
}, config.options); | ||
@@ -62,2 +65,8 @@ | ||
async _wait(restore_rate){ | ||
return new Promise((resolve, reject) => { | ||
setTimeout(resolve, restore_rate * 1000); | ||
}); | ||
} | ||
async refreshAccessToken(){ | ||
@@ -67,3 +76,3 @@ let res = await request({ | ||
url:'https://api.amazon.com/auth/o2/token', | ||
query:JSON.stringify({ | ||
body:JSON.stringify({ | ||
grant_type:'refresh_token', | ||
@@ -80,7 +89,7 @@ refresh_token:this._refresh_token, | ||
try { | ||
json_res = JSON.parse(res); | ||
json_res = JSON.parse(res.body); | ||
} catch (e){ | ||
throw new CustomError({ | ||
code:'REFRESH_ACCESS_TOKEN_PARSE_ERROR', | ||
message:res | ||
message:res.body | ||
}); | ||
@@ -98,3 +107,3 @@ } | ||
code:'UNKNOWN_REFRESH_ACCESS_TOKEN_ERROR', | ||
message:res | ||
message:res.body | ||
}); | ||
@@ -109,7 +118,7 @@ } | ||
try { | ||
json_res = xml_parser.parse(res); | ||
json_res = xml_parser.parse(res.body); | ||
} catch(e){ | ||
throw new CustomError({ | ||
code:'XML_PARSE_ERROR', | ||
message:res | ||
message:res.body | ||
}); | ||
@@ -132,3 +141,3 @@ } | ||
code:'NO_ROLE_CREDENTIALS_RECEIVED', | ||
message:res | ||
message:res.body | ||
}); | ||
@@ -138,13 +147,20 @@ } | ||
// req_params object params: | ||
// path: Required, the API path you want to request, [see SP API References](https://github.com/amzn/selling-partner-api-docs/tree/main/references) | ||
// method: Optional, HTTP Method of the call, default is GET | ||
// query: Optional, the input parameters of the call | ||
// req_params object: | ||
// * operation: Required, the operation you want to request [see SP API References](https://github.com/amzn/selling-partner-api-docs/tree/main/references) | ||
// * path: The input paramaters added to the path of the operation | ||
// * query: The input parameters added to the query string of the operation | ||
// * body: The input parameters added to the body of the operation | ||
async callAPI(req_params){ | ||
if (!req_params.path){ | ||
if (!req_params.operation){ | ||
throw new CustomError({ | ||
code:'NO_API_PATH_GIVEN', | ||
message:'Please provide an API path to call' | ||
code:'NO_OPERATION_GIVEN', | ||
message:'Please provide an operation to call' | ||
}); | ||
} | ||
if (!operations[req_params.operation]){ | ||
throw new CustomError({ | ||
code:'OPERATION_NOT_FOUND', | ||
message:'No operation found: ' + req_params.operation | ||
}); | ||
} | ||
if (this._options.auto_request_tokens){ | ||
@@ -164,12 +180,15 @@ if (!this._access_token){ | ||
} | ||
req_params = operations[req_params.operation](req_params); | ||
let signed_request = new Signer(this._region).signAPIRequest(this._access_token, this._role_credentials, req_params); | ||
let res = await request(signed_request); | ||
let json_res; | ||
if (res.statusCode === 204 && req_params.method === 'DELETE'){ | ||
return {success:true}; | ||
} | ||
try { | ||
// Replace newlines in json result because some errors are returned with newline characters, i.e. "InvalidSignature" | ||
json_res = JSON.parse(res.replace(/\n/g, '')); | ||
json_res = JSON.parse(res.body.replace(/\n/g, '')); | ||
} catch (e){ | ||
throw new CustomError({ | ||
code:'JSON_PARSE_ERROR', | ||
message:res | ||
message:res.body | ||
}); | ||
@@ -180,3 +199,3 @@ } | ||
// Refresh tokens when expired and auto_request_tokens is true | ||
if (error.code === 'Unauthorized'){ | ||
if (res.statusCode === 403 && error.code === 'Unauthorized'){ | ||
if (this._options.auto_request_tokens){ | ||
@@ -191,6 +210,11 @@ if (/access token.*expired/.test(error.details)){ | ||
} | ||
// Retry when call is throttled and auto_request_throttled is true | ||
} else if (res.statusCode === 429 && error.code === 'QuotaExceeded' && this._options.auto_request_throttled){ | ||
await this._wait(req_params.restore_rate); | ||
return await this.callAPI(req_params); | ||
} | ||
throw new CustomError(error); | ||
} | ||
return json_res.payload; | ||
// Some calls do not return response in payload but directly (i.e. operation "getSmallAndLightEligibilityBySellerSKU")! | ||
return json_res.payload || json_res; | ||
} | ||
@@ -197,0 +221,0 @@ |
@@ -68,14 +68,13 @@ const crypto = require('crypto-js'); | ||
_constructCanonicalRequestForAPI(access_token, method, path, encoded_query_string, query){ | ||
_constructCanonicalRequestForAPI(access_token, params, encoded_query_string){ | ||
let canonical = []; | ||
canonical.push(method); | ||
canonical.push(path); | ||
canonical.push(method.toLowerCase() === 'get' ? encoded_query_string : ''); | ||
canonical.push(params.method); | ||
canonical.push(params.api_path); | ||
canonical.push(encoded_query_string); | ||
canonical.push('host:' + this._api_endpoint); | ||
canonical.push('x-amz-access-token:' + access_token); | ||
canonical.push('x-amz-content-sha256:' + crypto.SHA256(encoded_query_string || '')); | ||
canonical.push('x-amz-date:' + this._iso_date.full); | ||
canonical.push(''); | ||
canonical.push('host;x-amz-access-token;x-amz-content-sha256;x-amz-date'); | ||
canonical.push(crypto.SHA256(method.toLowerCase() === 'get' ? '' : JSON.stringify(query))); | ||
canonical.push('host;x-amz-access-token;x-amz-date'); | ||
canonical.push(crypto.SHA256(params.body ? JSON.stringify(params.body) : '')); | ||
return canonical.join('\n'); | ||
@@ -101,5 +100,12 @@ } | ||
_constructURL(req_params, encoded_query_string){ | ||
let url = 'https://' + this._api_endpoint + req_params.api_path; | ||
if (encoded_query_string !== ''){ | ||
url += '?' + encoded_query_string; | ||
} | ||
return url; | ||
} | ||
signAPIRequest(access_token, role_credentials, req_params){ | ||
req_params.method = req_params.method || 'GET'; | ||
req_params.query = this._sortQuery(req_params.query); | ||
@@ -110,19 +116,16 @@ | ||
let encoded_query_string = this._constructEncodedQueryString(req_params.query); | ||
let canonical_request = this._constructCanonicalRequestForAPI(access_token, req_params.method, req_params.path, encoded_query_string, req_params.query); | ||
let canonical_request = this._constructCanonicalRequestForAPI(access_token, req_params, encoded_query_string); | ||
let string_to_sign = this._constructStringToSign(this._aws_regions[this._region], 'execute-api', canonical_request); | ||
let signature = this._constructSignature(this._aws_regions[this._region], 'execute-api', string_to_sign, role_credentials.secret); | ||
let get_params = (req_params.method.toLowerCase() !== 'get' || encoded_query_string === '') ? '' : ('?' + encoded_query_string); | ||
return { | ||
method:req_params.method, | ||
url:'https://' + this._api_endpoint + req_params.path + get_params, | ||
query:JSON.stringify(req_params.query), | ||
url:this._constructURL(req_params, encoded_query_string), | ||
body:req_params.body ? JSON.stringify(req_params.body) : null, | ||
headers:{ | ||
'Authorization':'AWS4-HMAC-SHA256 Credential=' + role_credentials.id + '/' + this._iso_date.short + '/' + this._aws_regions[this._region] + '/execute-api/aws4_request, SignedHeaders=host;x-amz-access-token;x-amz-content-sha256;x-amz-date, Signature=' + signature, | ||
'Content-Type': 'application/json', | ||
'Authorization':'AWS4-HMAC-SHA256 Credential=' + role_credentials.id + '/' + this._iso_date.short + '/' + this._aws_regions[this._region] + '/execute-api/aws4_request, SignedHeaders=host;x-amz-access-token;x-amz-date, Signature=' + signature, | ||
'Content-Type': 'application/json; charset=utf-8', | ||
'host':this._api_endpoint, | ||
'x-amz-access-token':access_token, | ||
'x-amz-security-token':role_credentials.security_token, | ||
'x-amz-content-sha256':crypto.SHA256(encoded_query_string).toString(crypto.enc.Hex), | ||
'x-amz-date':this._iso_date.full | ||
@@ -154,3 +157,3 @@ } | ||
url:'https://sts.amazonaws.com', | ||
query:encoded_query_string, | ||
body:encoded_query_string, | ||
headers:{ | ||
@@ -157,0 +160,0 @@ 'Authorization':'AWS4-HMAC-SHA256 Credential=' + aws_user.id + '/' + this._iso_date.short + '/us-east-1/sts/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=' + signature, |
{ | ||
"name": "amazon-sp-api", | ||
"version": "0.1.1", | ||
"version": "0.2.0", | ||
"description": "Amazon Selling Partner API client", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -70,3 +70,4 @@ # amazon-sp-api (client for the Amazon Selling Partner API) | ||
credentials_path:'<YOUR_CUSTOM_ABSOLUTE_PATH>', // Optional, a custom absolute path to your credentials file location | ||
auto_request_tokens:true // Optional, whether or not the client should retrieve new access and role credentials if non given or expired. Default is true | ||
auto_request_tokens:true, // Optional, whether or not the client should retrieve new access and role credentials if non given or expired. Default is true | ||
auto_request_throttled:true // Optional: Whether or not the client should automatically retry a request when throttled. Default is true | ||
} | ||
@@ -107,11 +108,13 @@ } | ||
The .callAPI() function takes an object as input: | ||
* path: Required, the API path you want to request, [see SP API References](https://github.com/amzn/selling-partner-api-docs/tree/main/references) | ||
* method: Optional, HTTP Method of the call, default is GET | ||
* query: Optional, the input parameters of the call | ||
* operation: Required, the operation you want to request [see SP API References](https://github.com/amzn/selling-partner-api-docs/tree/main/references) | ||
* path: The input paramaters added to the path of the operation | ||
* query: The input parameters added to the query string of the operation | ||
* body: The input parameters added to the body of the operation | ||
## Examples | ||
```javascript | ||
let res = await sellingPartner.callAPI({ | ||
path:'/sales/v1/orderMetrics', | ||
method:'GET', | ||
operation:'getOrderMetrics', | ||
query:{ | ||
marketplaceIds:'A1PA6795UKMFR9', | ||
marketplaceIds:['A1PA6795UKMFR9'], | ||
interval:'2020-10-01T00:00:00-07:00--2020-10-01T20:00:00-07:00', | ||
@@ -122,4 +125,24 @@ granularity:'Hour' | ||
``` | ||
```javascript | ||
let res = await sellingPartner.callAPI({ | ||
operation:'getCatalogItem', | ||
path:{ | ||
asin:'B084J4QQFT' | ||
}, | ||
query:{ | ||
MarketplaceId:'A1PA6795UKMFR9' | ||
} | ||
}); | ||
``` | ||
```javascript | ||
let res = await sellingPartner.callAPI({ | ||
operation:'createReport', | ||
body:{ | ||
reportType:'GET_FLAT_FILE_OPEN_LISTINGS_DATA', | ||
marketplaceIds:['A1PA6795UKMFR9'] | ||
} | ||
}); | ||
``` | ||
## Known Issues | ||
Since the Selling Partner API is still pretty new, not all API paths and endpoints have been tested for full functionality. If you find any calls not working please open up a new issue. |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
84461
33
2978
145
2