/** * Adapted to TypeScript by David A. Ball. (c) 2024. * * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://www.apache.org/licenses/LICENSE-2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ /** * ProductAdvertisingAPI * https://webservices.amazon.com/paapi5/documentation/index.html * */ import * as awsv4 from './auth/SignHelper.mjs'; import superagent from 'superagent'; import querystring from 'querystring'; export interface Stringifiable { toString(): string; } const PRODUCT_ADVERTISING_API = 'ProductAdvertisingAPI'; /** * @module ApiClient * @version 1.0.0 */ /** * Enumeration of collection format separator strategies. * @enum {String} * @readonly */ export enum CollectionFormatEnum { /** * Comma-separated values. Value: csv * @const */ CSV = ',', /** * Space-separated values. Value: ssv * @const */ SSV = ' ', /** * Tab-separated values. Value: tsv * @const */ TSV = '\t', /** * Pipe(|)-separated values. Value: pipes * @const */ PIPES = '|', /** * Native array. Value: multi * @const */ MULTI = 'multi', }; /** * Manages low level client-server communications, parameter marshalling, etc. There should not be any need for an * application to use this class directly - the *Api and model classes provide the public API for the service. The * contents of this file should be regarded as internal but are documented for completeness. * @alias module:ApiClient * @class */ export class ApiClient { /** * The default API client implementation. * @type {module:ApiClient} */ public static instance = new ApiClient(); /** * The base URL against which to resolve every API call's (relative) path. * @type {String} * @default https://webservices.amazon.com */ public basePath: string = 'https://webservices.amazon.com'.replace(/\/+$/, ''); /** * The Access Key ID which uniquely identifies you. * @type {String} */ public accessKey: string|null = null; /** * A key that is used in conjunction with the Access Key ID to cryptographically * sign an API request. * @type string */ public secretKey: string|null = null; /** * The host that receives the Amazon Product Advertising API GenNext request. * @type string * @default 'webservices.amazon.com' */ public host: string = 'webservices.amazon.com'; /** * The service's region where the request is targeted. * @type string * @default 'us-east-1' */ public region: string = 'us-east-1'; /** * The authentication methods to be included for all API calls. * @type {Array.} */ public authentications = { }; /** * The default HTTP headers to be included for all API calls. * @type {Array.} * @default {} */ public defaultHeaders = {}; /** * The default HTTP timeout for all API calls. * @type {Number} * @default 60000 */ public timeout = 60000; /** * If set to false an additional timestamp parameter is added to all API GET calls to * prevent browser caching * @type {Boolean} * @default true */ public cache = true; /** * If set to true, the client will save the cookies from each server * response, and return them in the next request. * @default false */ public enableCookies = false; /* * Allow user to override superagent agent */ public requestAgent = { 'User-Agent': 'dashersupply-fork-paapi5-nodejs-sdk/1.0.0' }; public agent = new superagent.agent(); /** * Returns a string representation for an actual parameter. * @param param The actual parameter. * @returns {String} The string representation of param. */ public paramToString(param: string|Stringifiable|Date|undefined|null) { if (param == undefined || param == null) { return ''; } if (param instanceof Date) { return param.toJSON(); } return param.toString(); }; /** * Builds full URL by appending the given path to the base URL and replacing path parameter place-holders with parameter values. * NOTE: query parameters are not handled here. * @param {String} path The path to append to the base URL. * @param {Object} pathParams The parameter values to append. * @returns {String} The encoded path with parameter values substituted. */ public buildUrl(path: string, pathParams: Record) { if (!path.match(/^\//)) { path = '/' + path; } var url = "https://" + this.host + path; var _this = this; url = url.replace(/\{([\w-]+)\}/g, function(fullMatch, key) { var value; if (pathParams.hasOwnProperty(key)) { value = _this.paramToString(pathParams[key]); } else { value = fullMatch; } return encodeURIComponent(value); }); return url; }; /** * Checks whether the given content type represents JSON.
* JSON content type examples:
*
    *
  • application/json
  • *
  • application/json; charset=UTF8
  • *
  • APPLICATION/JSON
  • *
* @param {String} contentType The MIME content type to check. * @returns {Boolean} true if contentType represents JSON, otherwise false. */ public isJsonMime(contentType: string) { return Boolean(contentType != null && contentType.match(/^application\/json(;.*)?$/i)); }; /** * Chooses a content type from the given array, with JSON preferred; i.e. return JSON if included, otherwise return the first. * @param {Array.} contentTypes * @returns {String} The chosen content type, preferring JSON. */ public jsonPreferredMime(contentTypes: string[]) { for (var i = 0; i < contentTypes.length; i++) { if (this.isJsonMime(contentTypes[i])) { return contentTypes[i]; } } return contentTypes[0]; }; /** * Checks whether the given parameter value represents file-like content. * @param param The parameter to check. * @returns {Boolean} true if param represents a file. */ public isFileParam(param: any) { // fs.ReadStream in Node.js and Electron (but not in runtime like browserify) if (typeof require === 'function') { var fs; try { fs = require('fs'); } catch (err) {} if (fs && fs.ReadStream && param instanceof fs.ReadStream) { return true; } } // Buffer in Node.js if (typeof Buffer === 'function' && param instanceof Buffer) { return true; } // Blob in browser if (typeof Blob === 'function' && param instanceof Blob) { return true; } // File in browser (it seems File object is also instance of Blob, but keep this for safe) if (typeof File === 'function' && param instanceof File) { return true; } return false; }; /** * Normalizes parameter values: *
    *
  • remove nils
  • *
  • keep files and arrays
  • *
  • format to string with `paramToString` for other cases
  • *
* @param {Object.} params The parameters as object properties. * @returns {Object.} normalized parameters. */ public normalizeParams(params: Record) { var newParams: Record = {}; for (var key in params) { if (params.hasOwnProperty(key) && params[key] != undefined && params[key] != null) { var value = params[key]; if (this.isFileParam(value) || Array.isArray(value)) { newParams[key] = value; } else { newParams[key] = this.paramToString(value); } } } return newParams; }; /** * Builds a string representation of an array-type actual parameter, according to the given collection format. * @param {Array} param An array parameter. * @param {module:ApiClient.CollectionFormatEnum} collectionFormat The array element separator strategy. * @returns {String|Array} A string representation of the supplied collection, using the specified delimiter. Returns * param as is if collectionFormat is multi. */ public buildCollectionParam(param: any[], collectionFormat: CollectionFormatEnum) { if (param == null) { return null; } switch (collectionFormat) { case CollectionFormatEnum.CSV: return param.map(this.paramToString).join(','); case CollectionFormatEnum.SSV: return param.map(this.paramToString).join(' '); case CollectionFormatEnum.TSV: return param.map(this.paramToString).join('\t'); case CollectionFormatEnum.PIPES: return param.map(this.paramToString).join('|'); case CollectionFormatEnum.MULTI: // return the array directly as SuperAgent will handle it as expected return param.map(this.paramToString); default: throw new Error('Unknown collection format: ' + collectionFormat); } }; /** * Deserializes an HTTP response body into a value of the specified type. * @param {Object} response A SuperAgent response object. * @param {(String|Array.|Object.|Function)} returnType The type to return. Pass a string for simple types * or the constructor function for a complex type. Pass an array containing the type name to return an array of that type. To * return an object, pass an object with one property whose name is the key type and whose value is the corresponding value type: * all properties on data will be converted to this type. * @returns A value of the specified type. */ public deserialize(response: superagent.Response, returnType: string|string[]|any|any[]) { if (response == null || returnType == null || response.status == 204) { return null; } // Rely on SuperAgent for parsing response body. // See http://visionmedia.github.io/superagent/#parsing-response-bodies var data = response.body; if (data == null || (typeof data === 'object' && typeof data.length === 'undefined' && !Object.keys(data).length)) { // SuperAgent does not always produce a body; use the unparsed response as a fallback data = response.text; } return ApiClient.convertToType(data, returnType); }; /** * Invokes the REST service using the supplied settings and parameters. * @param {String} path The base URL to invoke. * @param {String} httpMethod The HTTP method to use. * @param {String} apiName * @param {Object.} pathParams A map of path parameters and their values. * @param {Object.} queryParams A map of query parameters and their values. * @param {Object.} collectionQueryParams A map of collection query parameters and their values. * @param {Object.} headerParams A map of header parameters and their values. * @param {Object.} formParams A map of form parameters and their values. * @param {Object} bodyParam The value to pass as the request body. * @param {Array.} authNames An array of authentication type names. * @param {Array.} contentTypes An array of request MIME types. * @param {Array.} accepts An array of acceptable response MIME types. * @param {(String|Array|ObjectFunction)} returnType The required type to return; can be a string for simple types or the * constructor for a complex type. * @returns {Promise} A {@link https://www.promisejs.org/|Promise} object. */ public callApi(path: string, httpMethod: string, apiName: string, pathParams: Record, queryParams: Record, collectionQueryParams: Record, headerParams: Record, formParams: Record, bodyParam: any, authNames: string[], contentTypes: string[], accepts: string[], returnType: string|string[]|any|any[]) { // Throw error if credentials are not specified if (this.accessKey === undefined || this.secretKey === undefined || this.accessKey === null || this.secretKey === null) { throw new Error("Missing Credentials. Please specify accessKey and secretKey in client object."); } const self = this; const url = this.buildUrl(path, pathParams); const request = superagent(httpMethod, url); // apply authentications const region = this.region const service = PRODUCT_ADVERTISING_API; const timestamp = Date.now(); let requestHeaders = { 'content-encoding': 'amz-1.0', 'content-type': 'application/json; charset=utf-8', 'host': this.host, 'x-amz-target': 'com.amazon.paapi5.v1.ProductAdvertisingAPIv1.' + apiName, 'x-amz-date': awsv4.toAmzDate(timestamp) }; let authorizationHeader = awsv4.createAuthorizationHeader(this.accessKey, this.secretKey, requestHeaders, httpMethod, path, bodyParam, region, service, timestamp); let authHeader = { 'Authorization': authorizationHeader }; request.set(authHeader); // set collection query parameters for (const key in collectionQueryParams) { if (collectionQueryParams.hasOwnProperty(key)) { const param = collectionQueryParams[key]; if (param.collectionFormat === 'csv') { // SuperAgent normally percent-encodes all reserved characters in a query parameter. However, // commas are used as delimiters for the 'csv' collectionFormat so they must not be encoded. We // must therefore construct and encode 'csv' collection query parameters manually. if (param.value != null) { const value = param.value.map(this.paramToString).map(encodeURIComponent).join(','); request.query(encodeURIComponent(key) + "=" + value); } } else { // All other collection query parameters should be treated as ordinary query parameters. queryParams[key] = this.buildCollectionParam(param.value, param.collectionFormat); } } } // set query parameters if (httpMethod.toUpperCase() === 'GET' && this.cache === false) { queryParams['_'] = new Date().getTime(); } // set header parameters request.set(requestHeaders); // set requestAgent if it is set by user request.set(this.requestAgent); // set request timeout request.timeout(this.timeout); var contentType = 'application/json; charset=utf-8'; if (contentType) { // Issue with superagent and multipart/form-data (https://github.com/visionmedia/superagent/issues/746) if(contentType != 'multipart/form-data') { request.type(contentType); } } else if (!request.getHeader('Content-Type')) { request.type('application/json'); } if (contentType === 'application/x-www-form-urlencoded') { request.send(querystring.stringify(this.normalizeParams(formParams))); } else if (contentType == 'multipart/form-data') { var _formParams = this.normalizeParams(formParams); for (var key in _formParams) { if (_formParams.hasOwnProperty(key)) { if (this.isFileParam(_formParams[key])) { // file field request.attach(key, _formParams[key]); } else { request.field(key, _formParams[key]); } } } } else if (bodyParam) { request.send(bodyParam); } var accept = this.jsonPreferredMime(accepts); if (accept) { request.accept(accept); } if (returnType === 'Blob') { request.responseType('blob'); } else if (returnType === 'String') { request.responseType('string'); } // Attach previously saved cookies, if enabled if (this.enableCookies){ this.agent.attachCookies(request); } return new Promise(function(resolve, reject) { request.end(function(error, response) { if (error) { reject(error); } else { try { var data = self.deserialize(response, returnType); if (self.enableCookies && typeof window === 'undefined'){ self.agent.saveCookies(response); } resolve({data: data, response: response}); } catch (err) { reject(err); } } }); }); }; /** * Parses an ISO-8601 string representation of a date value. * @param {String} str The date value as a string. * @returns {Date} The parsed date object. */ public static parseDate(str: string): Date { return new Date(str.replace(/T/i, ' ')); }; /** * Converts a value to the specified type. * @param {(String|Object)} data The data to convert, as a string or object. * @param {(String|Array.|Object.|Function)} type The type to return. Pass a string for simple types * or the constructor function for a complex type. Pass an array containing the type name to return an array of that type. To * return an object, pass an object with one property whose name is the key type and whose value is the corresponding value type: * all properties on data will be converted to this type. * @returns An instance of the specified type or null or undefined if data is null or undefined. */ public static convertToType(data: string|any|any[], type: string|string[]|any|any[]) { if (data === null || data === undefined) return data switch (type) { case 'Boolean': return Boolean(data); case 'Integer': return parseInt(data, 10); case 'Number': return parseFloat(data); case 'String': return String(data); case 'Date': return ApiClient.parseDate(String(data)); case 'Blob': return data; default: if (type === Object) { // generic object, return directly return data; } else if (typeof type === 'function') { // for model type like: User return type.constructFromObject(data); } else if (Array.isArray(type)) { // for array type like: ['String'] var itemType = type[0]; return data.map(function(item: any) { return ApiClient.convertToType(item, itemType); }); } else if (typeof type === 'object') { // for plain object type like: {'String': 'Integer'} var keyType, valueType; for (var k in type) { if (type.hasOwnProperty(k)) { keyType = k; valueType = type[k]; break; } } var result: Record = {}; for (var k in data) { if (data.hasOwnProperty(k)) { var key = this.convertToType(k, keyType); var value = this.convertToType(data[k], valueType); result[key] = value; } } return result; } else { // for unknown type, return the data directly return data; } } }; /** * Constructs a new map or array model from REST data. * @param data {Object|Array} The REST data. * @param obj {Object|Array} The target object or array. */ public static constructFromObject(data: any|any[], obj: any|any[], itemType: string | string[] | any | any[]) { if (Array.isArray(data)) { for (var i = 0; i < data.length; i++) { if (data.hasOwnProperty(i)) obj[i] = ApiClient.convertToType(data[i], itemType); } } else { for (var k in data) { if (data.hasOwnProperty(k)) obj[k] = ApiClient.convertToType(data[k], itemType); } } }; }