'use strict';
const jsSHA = require("jssha");
const got = require('got');
const api_errors = require('./errors');
const requests = require('./requests');
const BATCH_MAX_SIZE = 10000;
/**
* Client for sending requests to Recombee and getting replies
*/
class ApiClient {
/**
* Construct the client
* @param {string} databaseId - ID of your database
* @param {string} secretToken - Corresponding secret token
* @param {Object} options - Other custom options
*/
constructor (databaseId, token, options) {
this.databaseId = databaseId;
this.token = token;
this.options = options || {};
if (Object.getPrototypeOf(this.options) !== Object.prototype) throw new Error(`options must be given as an Object (${this.options} given instead)`);
this.protocol = this.options.protocol || 'https';
this.baseUri = this._getBaseUri()
}
/**
* Send the request to Recombee
* @param {Request} request - Request to be sent
* @param {Object} callback - Optional callback (send returns Promise if omitted)
*/
send(request, callback) {
if (request instanceof requests.Batch && request.requests.length > BATCH_MAX_SIZE)
return this._send_multipart_batch(request);
let url = this._buildRequestUrl(request);
let options = {
method: request.method,
url: url,
headers: {'Accept': 'application/json',
'Content-Type': 'application/json',
'User-Agent': 'recombee-node-api-client/4.0.0'},
timeout: request.timeout
};
if (Object.entries(request.bodyParameters()).length > 0)
options.json = request.bodyParameters();
return got(options)
.json()
.then((response)=> {
return new Promise( (resolve) => {
if (callback) { return callback(null, response); }
return resolve(response);
});
})
.catch((error) => {
if (error instanceof got.HTTPError) {
error = new api_errors.ResponseError(request, error.response.statusCode, error.response.body);
}
else if (error instanceof got.RequestError) {
if(error.code === 'ETIMEDOUT' || error.code === 'ESOCKETTIMEDOUT') {
error = new api_errors.TimeoutError(request, error);
}
}
if (callback) {return callback(error)};
throw error;
});
}
_getRegionalBaseUri(region) {
const uri = {
'ap-se': 'rapi-ap-se.recombee.com',
'ca-east': 'rapi-ca-east.recombee.com',
'eu-west': 'rapi-eu-west.recombee.com',
'us-west': 'rapi-us-west.recombee.com'
}[region.toLowerCase()];
if (uri === undefined) {
throw new Error(`Region "${region}" is unknown. You may need to update the version of the SDK.`)
}
return uri;
}
_getBaseUri() {
let baseUri = process.env.RAPI_URI || this.options.baseUri;
if (this.options.region) {
if (baseUri) {
throw new Error('baseUri and region cannot be specified at the same time');
}
baseUri = this._getRegionalBaseUri(this.options.region);
}
return baseUri || 'rapi.recombee.com';
}
_buildRequestUrl(request) {
let usedProtocol = (request.ensureHttps) ? 'https' : this.protocol;
let reqUrl = request.path + this._encodeRequestQueryParams(request);
let signedUrl = this._signUrl(reqUrl);
return usedProtocol + '://' + this.baseUri + signedUrl;
}
_encodeRequestQueryParams(request) {
let res = ''
let queryParams = request.queryParameters();
let paramPairs = [];
for (let d in queryParams)
paramPairs.push(this._rfc3986EncodeURIComponent(d) + '=' + this._formatQueryParameterValue(queryParams[d]));
res += paramPairs.join('&');
if (res.length > 0) {
res = '?' + res;
}
return res;
}
//https://stackoverflow.com/questions/18251399/why-doesnt-encodeuricomponent-encode-single-quotes-apostrophes
_rfc3986EncodeURIComponent (str) {
return encodeURIComponent(str).replace(/[!'()*]/g, escape);
}
_formatQueryParameterValue(value) {
if (value instanceof Array) {
return value.map((v) => this._rfc3986EncodeURIComponent(v.toString())).join(',');
}
return this._rfc3986EncodeURIComponent(value.toString());
}
_split_requests(requests, chunk_size) {
//http://stackoverflow.com/questions/8495687/split-array-into-chunks
let result = [];
let i,j;
for (i=0,j=requests.length; i<j; i+=chunk_size) {
result.push(requests.slice(i,i+chunk_size));
}
return result;
}
_concat_multipart_results(responses) {
return new Promise(
function (resolve, reject) {
let result = [].concat.apply([], responses);
resolve(result);
}
);
}
_send_batch_part_rec(requests, results) {
if (requests.length == 0)
return new Promise((resolve) => {resolve(results)});
let request = requests.shift();
return this.send(request)
.then((result) => {
results.push(result);
return this._send_batch_part_rec(requests, results);
});
}
_send_multipart_batch(batch, callback) {
let chunks = this._split_requests(batch.requests, BATCH_MAX_SIZE);
let rqs = chunks.map((rqs) => new requests.Batch(rqs));
return this._send_batch_part_rec(rqs, [])
.then(this._concat_multipart_results)
.then((response)=> {
return new Promise( (resolve) => {
if (callback) { return callback(null, response); }
return resolve(response);
});
})
.catch((error) => {
if (callback) {return callback(error)};
throw error;
});
}
_signUrl (req_part) {
let url = '/' + this.databaseId + req_part;
url += (req_part.indexOf("?") == -1 ? "?" : "&" ) + "hmac_timestamp=" + parseInt(new Date().getTime() / 1000);
let shaObj = new jsSHA("SHA-1", "TEXT");
shaObj.setHMACKey(this.token, "TEXT");
shaObj.update(url);
url += "&hmac_sign=" + shaObj.getHMAC("HEX");
return url;
}
}
exports.ApiClient = ApiClient