amazon-sp-api
Version:
Amazon Selling Partner API client
804 lines (760 loc) • 31.8 kB
JavaScript
const CustomError = require('./CustomError');
const Request = require('./Request');
const {XMLParser} = require('fast-xml-parser/src/fxp');
const Credentials = require('./Credentials');
const endpoints = require('./endpoints');
const utils = require('./utils');
const csv = require('csvtojson');
const fs = require('fs');
const zlib = require('zlib');
const iconv = require('iconv-lite');
const client_version = require('../package.json').version;
const node_version = process.version;
const os = require('os');
// Provide credentials as environment variables OR create a path and file ~/.amzspapi/credentials (located in your user folder)
// If you don't provide an access_token, the first call to an API endpoint will request them with a TTL of 1 hour
// Tokens are reused for the class instance
// Retrieve the tokens via getters if you want to use them across multiple instances of the SellingPartner class
class SellingPartner {
// config object params:
// region:'<REGION>', // Required: The region to use for the SP-API endpoints. Must be one of: "eu", "na" or "fe"
// refresh_token:'<REFRESH_TOKEN>', // Optional: The refresh token of your app user. Required if "only_grantless_operations" option is set to "false".
// access_token:'<ACCESS_TOKEN>', // Optional: The temporary access token requested with the refresh token of the app user.
// endpoints_versions:{ // Optional: Defines the version to use for an endpoint as key/value pairs, i.e. "reports":"2021-06-30".
// ...
// },
// credentials:{ // Optional: The app client and aws user credentials. Should only be used if you have no means of using environment vars or credentials file!
// SELLING_PARTNER_APP_CLIENT_ID:'<APP_CLIENT_ID>',
// SELLING_PARTNER_APP_CLIENT_SECRET:'<APP_CLIENT_SECRET>'
// },
// options:{
// credentials_path:'~/.amzspapi/credentials', // Optional: A custom absolute path to your credentials file location.
// auto_request_tokens:true, // Optional: Whether or not the client should retrieve new access token if non given or expired.
// auto_request_throttled:true, // Optional: Whether or not the client should automatically retry a request when throttled.
// version_fallback:true, // Optional: Whether or not the client should try to use an older version of an endpoint if the operation is not defined for the desired version.
// use_sandbox:false, // Optional: Whether or not to use the sandbox endpoint.
// only_grantless_operations:false, // Optional: Whether or not to only use grantless operations.
// user_agent:'amazon-sp-api/<CLIENT_VERSION> (Language=Node.js/<NODE_VERSION>; Platform=<OS_PLATFORM>/<OS_RELEASE>)', // A custom user-agent header.
// debug_log:false, // Optional: Whether or not the client should print console logs for debugging purposes.
// timeouts:{
// response:0, // Optional: The time in milliseconds until a response timeout is fired (time between starting the request and receiving the first byte of the response).
// idle:0, // Optional: The time in milliseconds until an idle timeout is fired (time between receiving the last chunk and receiving the next chunk).
// deadline:0 // Optional: The time in milliseconds until a deadline timeout is fired (time between starting the request and receiving the full response).
// },
// retry_remote_timeout:true // Optional: Whether or not the client should retry a request to the remote server that failed with an ETIMEDOUT error
// https_proxy_agent:<HttpsProxyAgent> // Optional: A custom proxy agent
// }
constructor(config) {
this._region = config.region;
this._refresh_token = config.refresh_token;
this._access_token = config.access_token;
// Will hold access tokens for grantless operations (with scope as key)
this._grantless_tokens = {};
this._options = Object.assign(
{
auto_request_tokens: true,
auto_request_throttled: true,
use_sandbox: false,
only_grantless_operations: false,
version_fallback: true,
user_agent: `amazon-sp-api/${client_version} (Language=Node.js/${node_version}; Platform=${os.type()}/${os.release()})`,
debug_log: false,
timeouts: {},
retry_remote_timeout: true
},
config.options
);
this._current_call_timeouts = this._options.timeouts;
this._endpoints_versions = this._validateEndpointsVersions(Object.assign({}, config.endpoints_versions));
this._credentials = new Credentials(
config.credentials,
this._options.credentials_path,
this._options.debug_log
).load();
this._xml_parser = new XMLParser();
if (!this._region || !/^(eu|na|fe)$/.test(this._region)) {
throw new CustomError({
code: 'NO_VALID_REGION_PROVIDED',
message: 'Please provide one of: "eu", "na" or "fe"'
});
}
if (!this._refresh_token && !this._options.only_grantless_operations) {
throw new CustomError({
code: 'NO_REFRESH_TOKEN_PROVIDED',
message: 'Please provide a refresh token or set "only_grantless_operations" option to true'
});
}
this._request = new Request(this._region, this._options);
}
get access_token() {
return this._access_token;
}
get endpoints() {
return endpoints;
}
// Make sure that all defined endpoints and its defined versions exist
_validateEndpointsVersions(endpoints_versions) {
let invalid_endpoints = Object.keys(endpoints_versions).filter((endpoint) => {
return !endpoints[endpoint];
});
if (invalid_endpoints.length) {
throw new CustomError({
code: 'VERSION_DEFINED_FOR_INVALID_ENDPOINTS',
message: `One or more endpoints are not valid. These endpoints don't exist: ${invalid_endpoints.join(',')}`
});
}
let invalid_endpoints_versions = Object.keys(endpoints_versions).filter((endpoint) => {
return !endpoints[endpoint].__versions.includes(endpoints_versions[endpoint]);
});
if (invalid_endpoints_versions.length) {
throw new CustomError({
code: 'INVALID_VERSION_FOR_ENDPOINTS',
message: `The provided version for the following endpoint(s) is not valid: ${invalid_endpoints_versions.join(
','
)}`
});
}
return endpoints_versions;
}
async _wait(restore_rate) {
return new Promise((resolve, reject) => {
setTimeout(resolve, restore_rate * 1000);
});
}
async _unzip(buffer) {
return new Promise((resolve, reject) => {
zlib.gunzip(buffer, (err, unzipped_buffer) => {
if (err) {
reject(err);
}
resolve(unzipped_buffer);
});
});
}
async _saveFile(content, options) {
return new Promise((resolve, reject) => {
if (options.json) {
content = JSON.stringify(content);
}
fs.writeFile(options.file, content, (err) => {
err ? reject(err) : resolve();
});
});
}
async _readFile(file, content_type) {
return new Promise((resolve, reject) => {
let regexp_charset = /charset=([^;]*)/;
let content_match = content_type.match(regexp_charset);
let encoding = content_match && content_match[1] ? content_match[1] : 'utf-8';
// fs.readFile doesn't accept ISO-8859-1 as encoding value --> use latin1 as value which is the same
if (encoding.toUpperCase() === 'ISO-8859-1') {
encoding = 'latin1';
}
fs.readFile(file, encoding, (err, content) => {
err ? reject(err) : resolve(content);
});
});
}
_validateDocumentDetails(details) {
if (!details || !details.url) {
throw new CustomError({
code: 'DOCUMENT_INFORMATION_MISSING',
message: 'Please provide url'
});
}
let compression = details.compressionAlgorithm;
// Docs state that no other zip standards should be possible, but check if its correct anyway
if (compression && compression !== 'GZIP') {
throw new CustomError({
code: 'UNKNOWN_ZIP_STANDARD',
message: `Cannot unzip ${compression}, expecting GZIP`
});
}
}
_validateUpOrDownloadSuccess(res, request_type) {
if (res.statusCode !== 200) {
let json_res;
try {
json_res = this._xml_parser.parse(res.body);
} catch (e) {
throw new CustomError({
code: `${request_type}_ERROR`,
message: res.body
});
}
if (json_res && json_res.Error) {
throw new CustomError({
code: json_res.Error.Code,
message: json_res.Error.Message
});
} else {
throw new CustomError({
code: `${request_type}_ERROR`,
message: json_res
});
}
}
}
// Decode buffer with given charset
_decodeBuffer(decompressed_buffer, headers, charset) {
// Try to extract charset from header if no charset explicitly defined
if (!charset && headers && headers['content-type']) {
let charset_match = headers['content-type'].match(/\.*charset=([^;]*)/);
if (charset_match && charset_match[1]) {
charset = charset_match[1];
}
}
// Use utf8 as default charset if no charset given by options or in headers
if (!charset) {
charset = 'utf8';
}
try {
return iconv.decode(decompressed_buffer, charset);
} catch (e) {
throw new CustomError({
code: 'DECODE_ERROR',
message: e.message
});
}
}
_constructRefreshAccessTokenBody(scope) {
let body = {
client_id: this._credentials.app_client.id,
client_secret: this._credentials.app_client.secret
};
let valid_scopes = ['sellingpartnerapi::notifications', 'sellingpartnerapi::client_credential:rotation'];
if (scope) {
// Make sure that scope is valid
if (!valid_scopes.includes(scope)) {
throw new CustomError({
code: 'INVALID_SCOPE_ERROR',
message: `"Scope for requesting token for grantless operations is invalid. Please provide one of: ${valid_scopes.join(
','
)}`
});
}
body.grant_type = 'client_credentials';
body.scope = scope;
} else if (!this._options.only_grantless_operations) {
body.grant_type = 'refresh_token';
body.refresh_token = this._refresh_token;
} else {
throw new CustomError({
code: 'NO_SCOPE_PROVIDED',
message: `"Grantless tokens require a scope. Please provide one of: ${valid_scopes.join(',')}`
});
}
return JSON.stringify(body);
}
_tokenExists(scope) {
return (this._access_token && !scope) || (scope && this._grantless_tokens[scope]);
}
async _validateAccessToken(scope) {
if (this._options.auto_request_tokens) {
if (!this._tokenExists(scope)) {
await this.refreshAccessToken(scope);
}
}
if (!this._tokenExists(scope)) {
throw new CustomError({
code: 'NO_ACCESS_TOKEN_PRESENT',
message:
'Did you turn off "auto_request_tokens" and forgot to refresh the access token or the scope for a grantless token?'
});
}
}
_validateMethod(method) {
if (!method || !/^(GET|POST|PUT|DELETE|PATCH)$/.test(method.toUpperCase())) {
throw new CustomError({
code: 'NO_VALID_METHOD_PROVIDED',
message: 'Please provide a valid HTTP Method ("GET","POST","PUT","DELETE" or "PATCH") when using "api_path"'
});
}
return method.toUpperCase();
}
_validateOperationAndEndpoint(operation, endpoint) {
if (!operation) {
throw new CustomError({
code: 'NO_OPERATION_GIVEN',
message: 'Please provide an operation to call'
});
}
// Split operation in endpoint and operation if shorthand dot notation
if (operation.includes('.')) {
let op_split = operation.split('.');
endpoint = op_split[0];
operation = op_split[1];
} else if (!endpoint) {
throw new CustomError({
code: 'NO_ENDPOINT_GIVEN',
message: 'Please provide an endpoint to call'
});
}
if (!endpoints[endpoint]) {
throw new CustomError({
code: 'ENDPOINT_NOT_FOUND',
message: `No endpoint found: ${endpoint}`
});
}
if (!endpoints[endpoint].__operations.includes(operation)) {
throw new CustomError({
code: 'INVALID_OPERATION_FOR_ENDPOINT',
message: `The operation ${operation} is not valid for endpoint ${endpoint}`
});
}
return {operation, endpoint};
}
_getFallbackVersion(operation, endpoint, version) {
// Make sure to only look for the operation in older versions
// --> we don't want to break stuff by accidently calling a newer version than expected!
let version_index = endpoints[endpoint].__versions.indexOf(version);
let fallback_version = endpoints[endpoint].__versions
.slice(0, version_index)
.reverse()
.find((__version) => {
return endpoints[endpoint][__version][operation];
});
// Throw error if version_fallback is disabled or no fallback version was found
if (!this._options.version_fallback || !fallback_version) {
throw new CustomError({
code: 'OPERATION_NOT_FOUND_FOR_VERSION',
message: `Operation ${operation} not found for version ${version}`
});
}
return fallback_version;
}
// Logic if version was explicitly set in .callAPI options
_validateLocallySetVersion(operation, endpoint, version) {
// Throw error if the explicitly specified version in .callAPI can't be found for the endpoint
if (!endpoints[endpoint].__versions.includes(version)) {
throw new CustomError({
code: 'INVALID_VERSION',
message: `Invalid version ${version} for endpoint ${endpoint} and operation ${operation}. Should be one of: ${endpoints[
endpoint
].__versions.join('","')}`
});
}
// If operation is not supported for the version:
// --> try to find an older version of the endpoint that supports the operation
if (!endpoints[endpoint][version][operation]) {
return this._getFallbackVersion(operation, endpoint, version);
}
return version;
}
// Logic if version was NOT explicitly set in .callAPI options
_validateGloballySetVersion(operation, endpoint) {
// If no version for the endpoint was set in constructor config:
// --> find the oldest version that supports the operation
if (!this._endpoints_versions[endpoint]) {
// We can directly return the version as its impossible for a valid operation to have no version
return endpoints[endpoint].__versions.find((__version) => {
return endpoints[endpoint][__version][operation];
});
}
// Get the version specified for the endpoint in constructor config
let version = this._endpoints_versions[endpoint];
// If operation is not supported for the version:
// --> try to find an older version of the endpoint that supports the operation
if (!endpoints[endpoint][version][operation]) {
return this._getFallbackVersion(operation, endpoint, version);
}
return version;
}
_validateAndGetVersion(operation, endpoint, version) {
return version
? this._validateLocallySetVersion(operation, endpoint, version)
: this._validateGloballySetVersion(operation, endpoint);
}
_validateOperationAllowance(scope) {
if (this._options.only_grantless_operations && !scope) {
throw new CustomError({
code: 'INVALID_OPERATION_ERROR',
message:
'Operation is not grantless. Set "only_grantless_operations" to false and provide a "refresh_token" to be able to call the operation.'
});
}
}
_constructExchangeBody(auth_code) {
if (!auth_code) {
throw new CustomError({
code: 'NO_AUTH_CODE_PROVIDED',
message:
'Please provide an authorization code (spapi_auth_code) operation to exchange it for a "refresh_token".'
});
}
let body = {
grant_type: 'authorization_code',
code: auth_code,
client_id: this._credentials.app_client.id,
client_secret: this._credentials.app_client.secret
};
return JSON.stringify(body);
}
async _createReport(req_params) {
let res = await this.callAPI({
operation: 'reports.createReport',
body: req_params.body,
options: {
...(req_params.version ? {version: req_params.version} : {})
}
});
return res.reportId;
}
async _cancelReport(req_params, report_id) {
await this.callAPI({
operation: 'reports.cancelReport',
path: {
reportId: report_id
},
options: {
...(req_params.version ? {version: req_params.version} : {})
}
});
}
async _getReport(req_params, report_id) {
let res = await this.callAPI({
operation: 'reports.getReport',
path: {
reportId: report_id
},
options: {
...(req_params.version ? {version: req_params.version} : {})
}
});
if (res.processingStatus === 'DONE') {
return res.reportDocumentId;
} else if (['CANCELLED', 'FATAL'].includes(res.processingStatus)) {
throw new CustomError({
code: 'REPORT_PROCESSING_' + res.processingStatus,
message: 'Something went wrong while processing the report.'
});
} else {
req_params.tries++;
if (this._options.debug_log) {
console.log(
`Current status of report ${req_params.body.reportType}: ${res.processingStatus} (tries: ${req_params.tries})`
);
}
let interval = req_params.interval || 10000;
if (!req_params.cancel_after || req_params.cancel_after > req_params.tries) {
await this._wait(interval / 1000);
return await this._getReport(req_params, report_id);
} else {
await this._cancelReport(req_params, report_id);
throw new CustomError({
code: 'REPORT_PROCESSING_CANCELLED_MANUALLY',
message: `Report did not finish after ${req_params.tries} tries (interval ${interval} ms).`
});
}
}
}
async _getReportDocument(req_params, report_document_id) {
let res = await this.callAPI({
operation: 'reports.getReportDocument',
path: {
reportDocumentId: report_document_id
},
options: {
...(req_params.version ? {version: req_params.version} : {})
}
});
return res;
}
async _retryThrottledRequest(req_params, res) {
// Wait the restore rate before retrying the call if dynamic or static restore rate is set
if (res?.headers?.['x-amzn-ratelimit-limit'] || req_params.restore_rate) {
// Use dynamic restore rate from result header if given --> otherwise use defined default restore_rate of the operation
let restore_rate = res?.headers?.['x-amzn-ratelimit-limit']
? 1 / (res.headers['x-amzn-ratelimit-limit'] * 1)
: req_params.restore_rate;
if (this._options.debug_log) {
console.log(
`Request throttled, retrying a call of ${
req_params.operation || req_params.api_path
} in ${restore_rate} seconds...`
);
}
await this._wait(restore_rate);
}
return await this.callAPI(req_params);
}
// Exchange an authorization code (spapi_oauth_code) for a refresh token
async exchange(auth_code) {
let res = await this._request.execute({
method: 'POST',
url: 'https://api.amazon.com/auth/o2/token',
body: this._constructExchangeBody(auth_code),
headers: {
'Content-Type': 'application/json'
}
});
let json_res;
try {
json_res = JSON.parse(res.body);
} catch (e) {
throw new CustomError({
code: 'EXCHANGE_AUTH_CODE_PARSE_ERROR',
message: res.body
});
}
if (json_res.error) {
throw new CustomError({
code: json_res.error,
message: json_res.error_description
});
}
return json_res;
}
// If scope is provided a token for a grantless operation is requested
// scope should be one of: ['sellingpartnerapi::notifications', 'sellingpartnerapi::client_credential::rotation']
async refreshAccessToken(scope) {
let res = await this._request.execute({
method: 'POST',
url: 'https://api.amazon.com/auth/o2/token',
body: this._constructRefreshAccessTokenBody(scope),
headers: {
'Content-Type': 'application/json'
},
timeouts: this._current_call_timeouts
});
let json_res;
try {
json_res = JSON.parse(res.body);
} catch (e) {
throw new CustomError({
code: 'REFRESH_ACCESS_TOKEN_PARSE_ERROR',
message: res.body
});
}
if (json_res.access_token) {
if (!scope) {
this._access_token = json_res.access_token;
} else {
this._grantless_tokens[scope] = json_res.access_token;
}
} else if (json_res.error) {
throw new CustomError({
code: json_res.error,
message: json_res.error_description
});
} else {
throw new CustomError({
code: 'UNKNOWN_REFRESH_ACCESS_TOKEN_ERROR',
message: res.body
});
}
}
// req_params object:
// operation:'<OPERATION_TO_CALL>', // Optional: The operation you want to request. May also include endpoint as shorthand dot notation. Required if "api_path" is not defined.
// endpoint:'<ENDPOINT_OF_OPERATION>', // Optional: The endpoint of the operation. Required if endpoint is not part of operation as shorthand dot notation and if "api_path" is not defined.
// path:{ // Optional: The input paramaters added to the path of the operation.
// ...
// },
// query:{ // Optional: The input paramaters added to the query string of the operation.
// ...
// },
// body:{ // Optional: The input paramaters added to the body of the operation.
// ...
// },
// api_path:'<FULL_PATH_OF_OPERATION>', // Optional: The full path of an operation. Required if "operation" is not defined.
// method:'GET' // The HTTP method to use. Required only if "api_path" is defined. Must be one of: "GET", "POST", "PUT", "DELETE" or "PATCH".
// restricted_data_token:'<RESTRICTED_DATA_TOKEN>' // Optional: A token received from a "createRestrictedDataToken" operation for receiving PII from a restricted operation.
// options:{
// version:'<OPERATION_ENDPOINT_VERSION>', // Optional: The endpoint’s version that should be used when calling the operation. Will be preferred over an "endpoints_versions" setting.
// restore_rate:'<RESTORE_RATE_IN_SECONDS>', // Optional: The restore rate (in seconds) that should be used when calling the operation. Will be preferred over the default restore rate of the operation.
// raw_result:false // Whether or not the client should return the "raw" result, which will include the raw body, buffer chunks, statuscode and headers of the result.
// }
async callAPI(req_params) {
let options = Object.assign({}, req_params.options);
if (req_params.api_path) {
req_params.method = this._validateMethod(req_params.method);
} else {
let {operation, endpoint} = this._validateOperationAndEndpoint(req_params.operation, req_params.endpoint);
let version = this._validateAndGetVersion(operation, endpoint, options.version);
req_params = {
...endpoints[endpoint][version][operation](req_params),
...(req_params.headers || {})
};
if (req_params.deprecation_date) {
utils.warn('DEPRECATION', req_params.deprecation_date);
}
if (!this._options.use_sandbox && req_params.sandbox_only) {
utils.warn('SANDBOX_ONLY', operation);
}
}
// Use user-defined restore_rate if specified, otherwise use default for operation
if (options.restore_rate && !isNaN(options.restore_rate)) {
req_params.restore_rate = options.restore_rate;
}
// Overwrite global timeouts definitions by call specific timeout options
req_params.timeouts = Object.assign({}, this._options.timeouts, options.timeouts);
// Store timeouts defined for the current call to use for any other requests we may need to make e.g. refresh access token
this._current_call_timeouts = req_params.timeouts;
// Scope will only be defined for grantless operations
let scope = req_params.scope;
this._validateOperationAllowance(scope);
await this._validateAccessToken(scope);
// Make sure to use the correct token for the request
let token_for_request = this._access_token;
if (scope) {
token_for_request = this._grantless_tokens[scope];
} else if (req_params.restricted_data_token) {
token_for_request = req_params.restricted_data_token;
}
let res = await this._request.api(token_for_request, req_params);
if (options.raw_result) {
return res;
}
if (res.statusCode === 204) {
return {success: true};
}
let json_res;
try {
json_res = JSON.parse(res.body.replace(/\n/g, ''));
} catch (e) {
throw new CustomError({
code: 'JSON_PARSE_ERROR',
message: res.body
});
}
if (json_res.errors?.length) {
let error = json_res.errors[0];
// Refresh tokens when expired and auto_request_tokens is true
if (res.statusCode === 403) {
if (error.code === 'Unauthorized' && this._options.auto_request_tokens) {
if (/access token.*expired/.test(error.details)) {
if (this._options.debug_log) {
console.log('Access token expired, refreshing it now');
}
await this.refreshAccessToken(scope);
return await this.callAPI(req_params);
}
}
// Retry when call is throttled and auto_request_throttled is true
} else if (res.statusCode === 429 && error.code === 'QuotaExceeded' && this._options.auto_request_throttled) {
return await this._retryThrottledRequest(req_params, res);
} else if (error.code === 'InternalFailure' && this._options.use_sandbox) {
throw new CustomError({
code: 'INVALID_SANDBOX_PARAMETERS',
message:
"You're in SANDBOX mode, make sure sandbox parameters are correct, as in Amazon SP API documentation: https://github.com/amzn/selling-partner-api-docs/blob/main/guides/developer-guide/SellingPartnerApiDeveloperGuide.md#how-to-make-a-sandbox-call-to-the-selling-partner-api"
});
}
throw new CustomError(error);
}
// If there is a pagination outside payload (like for getInventorySummaries), this will include it with the result
if (json_res.pagination && json_res.payload) {
return Object.assign(json_res.pagination, json_res.payload);
}
// Some calls do not return response in payload but directly (i.e. operation "getSmallAndLightEligibilityBySellerSKU")!
return json_res.payload || json_res;
}
// Download a report or feed result
// Options object:
// json:false, // Optional: Whether or not the content should be transformed to json before returning it (from tab delimited flat-file or XML).
// unzip:true, // Optional: Whether or not the content should be unzipped before returning it.
// file:'<FILE_PATH>', // Optional: The absolute file path to save the report to. Even when saved to disk the report content is still returned.
// charset:'utf8' // Optional: The charset to use for decoding the content. Is ignored when content is compressed and unzip is set to false.
async download(details, options = {}) {
options = Object.assign(
{
unzip: true
},
options
);
this._validateDocumentDetails(details);
// Result will be a tab-delimited flat file or an xml document
let res = await this._request.execute({
url: details.url,
timeouts: options.timeouts
});
this._validateUpOrDownloadSuccess(res, 'DOWNLOAD');
// Decompress if content is compressed and unzip option is true
let decoded =
details.compressionAlgorithm && options.unzip
? await this._unzip(Buffer.concat(res.chunks))
: Buffer.concat(res.chunks);
if (!details.compressionAlgorithm || options.unzip) {
decoded = this._decodeBuffer(decoded, res.headers, options.charset);
if (options.json) {
if (res.headers['content-type'] === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
throw new CustomError({
code: 'PARSE_ERROR',
message: "Report is a .xlsx file. Could not parse result to JSON. Remove the 'json:true' option."
});
}
// Transform content to json --> take content type from which to transform to json from result header
try {
if (res.headers['content-type'].includes('xml')) {
decoded = this._xml_parser.parse(decoded);
} else if (res.headers['content-type'].includes('plain')) {
// Some reports are returned in JSON format with content-type text/plain (i.e. "GET_V2_SELLER_PERFORMANCE_REPORT")
try {
let json_res = JSON.parse(decoded);
decoded = json_res;
} catch {
decoded = await csv({
delimiter: '\t',
quote: 'off'
}).fromString(decoded);
}
}
} catch (e) {
throw new CustomError({
code: 'PARSE_ERROR',
message: 'Could not parse result to JSON.',
details: decoded
});
}
}
}
if (options.file) {
await this._saveFile(decoded, options);
}
return decoded;
}
// Upload a tab-delimited flat file or an xml document
// Feed object:
// content:'<CONTENT>', // Optional: The content to upload as a string. Required if "file" is not provided.
// file:'<FILE_PATH>', // Optional: The absolute file path to the feed content document to upload. Required if "content" is not provided.
// contentType:'<CONTENT_TYPE>' // Optional: The contentType of the content to upload. Should be one of "text/xml" or "text/tab-separated-values" and the charset of the content, i.e. "text/xml; charset=utf-8".
async upload(details, feed) {
this._validateDocumentDetails(details);
if (!feed || (!feed.content && !feed.file)) {
throw new CustomError({
code: 'NO_FEED_CONTENT_PROVIDED',
message: 'Please provide "content" (string) or "file" (absolute path) of feed.'
});
}
if (!feed.contentType) {
throw new CustomError({
code: 'NO_FEED_CONTENT_TYPE_PROVIDED',
message:
'Please provide "contentType" of feed (should be identical to the contentType used in "createFeedDocument" operation).'
});
}
let feed_content = feed.content || (await this._readFile(feed.file, feed.contentType));
// Upload content
let res = await this._request.execute({
url: details.url,
method: 'PUT',
headers: {
'Content-Type': feed.contentType
},
body: Buffer.from(feed_content)
});
this._validateUpOrDownloadSuccess(res, 'UPLOAD');
return {success: true};
}
async downloadReport(req_params) {
req_params.tries = 0;
let report_id = await this._createReport(req_params);
let report_document_id = await this._getReport(req_params, report_id);
let report_document = await this._getReportDocument(req_params, report_document_id);
return await this.download(report_document, req_params.download || {});
}
updateCredentials(credentials) {
this._credentials = new Credentials(credentials, this._options.credentials_path, this._options.debug_log).load();
}
}
module.exports = SellingPartner;