diff --git a/lib/api/README.md b/lib/api/README.md index 728e644..6adb078 100644 --- a/lib/api/README.md +++ b/lib/api/README.md @@ -131,4 +131,63 @@ getStatistics({ --- +--- + +### swatGetStatistics + +Returns the statistics for a given list of products. API call can be made to apps.bazaarvoice.com or api.bazaarvoice.com + +#### Syntax + +```javascript +api.get('swatStatistics', options) + +// or +const getStatistics = require('bv-ui-core/lib/api/swatStatistics') +swatGetStatistics(options) +``` + +#### Parameters + +##### options + +An object with key/value pairs of inputs. Most values are required. + +##### options.productIds + +An array of product ids to query for. A warning will be emitted if over 100 products are requested. + +##### options.useBackend +A boolean value. If set to true, the call will be made to apps.bazaarvoice.com. If set to false, the call will be made to api.bazaarvoice.com. + +##### options.environment + +A string representing the data environment. Accepts `qa`, `staging` or +`production`. + +##### options.key + +Your api key. + +##### options.type + +A string representing the type of statistics you want to query for. Currently +accepts `Reviews` or `NativeReviews`. + +Using `Reviews` returns statistics for all content, including syndicated +content (if enabled on your API key). If you only want statistics for reviews +you own that were written for the products specified, use `NativeReviews` +instead. + +##### options.filters (optional) + +An object representing filters keyed by the name of the filter. `ContentLocale ` +can be provided to specify a locale subset for the statistics. If no +`ContentLocale` is provided, will return the global statistics for each product +in the query. + + + +---- + [0]: https://developer.bazaarvoice.com/docs/read/conversations diff --git a/lib/api/swatStatistics.js b/lib/api/swatStatistics.js new file mode 100644 index 0000000..4fd73cc --- /dev/null +++ b/lib/api/swatStatistics.js @@ -0,0 +1,157 @@ +/* global fetch */ +/** + * @fileOverview Module for interacting with the statistcs.json endpoint from both api.bazaarvoice.com and apps.bazaarvoice.com + * + */ +const envPrefixMap = { + qa: 'qa.', + staging: 'stg.', + production: '.' +}; + +// Default configuration +const API_VERSION = 5.4; +const MAX_REQUESTED_IDS = 100; + + /** + * Calls the statistics.json API with provided options. Returns a promise that + * is fulfilled with an array of Results from the API or rejected with the error + * message. + * + * Example: + * + * getStatistics({ + * productIds: ['product1', 'product2', 'product3'], + * environment: 'qa', + * key: 'clients_api_key', + * type: 'Reviews', + * filters: { + * ContentLocale: 'en_US' + * } + * }).then(results => { + * // Do something with results + * }, errorMsg => { + * // Do something with error. + * }) + * + * https://developer.bazaarvoice.com/docs/read/conversations/statistics/display/5_4 + * + * @param {Object} options object that contains options + * @param {Array} options.productIds - array of product IDs + * @param {Array} options.environment - the data environment + * @param {Array} options.key - client's api key + * @param {string} options.type - Using "Reviews" returns statistics for all + * content, including syndicated content (if enabled on your API key). + * If you only want statistics for reviews you own that were written for + * the products specified, use "NativeReviews" instead. + * @param {Object} [options.filters] - Filters object. Keyed by filter name. + * @return {Promise} a promise that will be resolved with the raw results + * from the statistics call. + */ +function swatGetStatistics ({ + productIds, + useBackend, + environment, + key, + type, + incentivized = false, + filters = {} + }) { + if (!productIds || !Array.isArray(productIds)) { + throw new TypeError('productIds must be an array'); + } + + const envPrefix = envPrefixMap[environment]; + if (!envPrefix) { + throw new TypeError('environment must be "qa", "staging", or "production"'); + } + + if (!key) { + throw new TypeError('key must be provided'); + } + + if (!type || !(type === 'Reviews' || type === 'NativeReviews')) { + throw new TypeError('type must be "Reviews" or "NativeReviews"'); + } + let basePath = '' + if (useBackend) { + basePath = `https://apps-${envPrefix}bazaarvoice.com/api/data/statistics.json` + } + else { + basePath = `https://${envPrefix}api.bazaarvoice.com/data` + } + + + let uri = + `${basePath}` + + `?apiversion=${API_VERSION}` + + `&passkey=${key}` + + `&stats=${type}` + + Object.keys(filters) + .map(filter => `&filter=${filter}:${filters[filter]}`) + .join(); + + // We should add `incentivizedStats` request param in case + // if `incentivized` flag is enabled that means such reviews + // have to be loaded in response + if (incentivized) { + uri = uri + `&incentivizedStats=${incentivized}`; + } + + // Clone the productIds so we can manipulate it. + productIds = [...productIds]; + const productIdChunks = []; + + if (productIds.length > 100) { + console.warn('Requesting more than 100 products is not recommended!'); + } + + while (productIds.length > 0) { + productIdChunks.push(productIds.splice(0, MAX_REQUESTED_IDS)); + } + + return Promise.all( + productIdChunks.map( + products => + new Promise((resolve, reject) => { + const requestUri = `${uri}&filter=ProductId:${products.join(',')}`; + return fetch(requestUri) + .then(response => response.json()) + .then(json => { + // If there are errors in the actual response body (from API) + // we need to handle them here. + if (json.HasErrors) { + let errors = json.Errors; + // If for some reason errors is empty or doesn't exist, + if (!errors || !Array.isArray(errors) || errors.length <= 0) { + reject({ + Message: 'An unknown error occurred.', + Code: 'ERROR_UNKNOWN', + }); + } + else { + // We can reasonably assume that if an error occurred for this + // request it should only have a single error response + // unlike submission api errors, which have multiples. + reject(json.Errors[0]); + } + } + else { + resolve(json.Results); + } + }); + }) + ) + ).then(results => { + if (results.length === 0) { + return results; + } + else { + return results.reduce((a, b) => { + return a.concat(b); + }); + } + }); +} + +module.exports = swatGetStatistics; \ No newline at end of file diff --git a/lib/bvFetch/index.js b/lib/bvFetch/index.js index e7bf8b7..dfdd5ac 100644 --- a/lib/bvFetch/index.js +++ b/lib/bvFetch/index.js @@ -133,7 +133,10 @@ module.exports = function BvFetch ({ shouldCache, cacheName, cacheLimit }) { .then((cache) => { return cache.match(cacheKey) .then((cachedResponse) => { - + if (!cachedResponse) { + this.cachedUrls.delete(cacheKey) + return Promise.resolve(null); + } const cachedTime = cachedResponse.headers.get('X-Bazaarvoice-Cached-Time'); const ttl = cachedResponse.headers.get('Cache-Control').match(/max-age=(\d+)/)[1]; const currentTimestamp = Date.now();