UNPKG

@windingtree/wt-search-api

Version:

NodeJS app that enables quick search over data from Winding Tree platform

147 lines (132 loc) 4.43 kB
const request = require('request-promise-native'); const { readApiUrl } = require('../../config'); const DESCRIPTION_FIELDS = [ 'id', 'managerAddress', 'name', 'description', 'location', 'images', 'contacts', 'address', 'roomTypes', 'timezone', 'currency', 'amenities', 'updatedAt', 'defaultCancellationAmount', 'cancellationPolicies', 'notificationsUri', 'bookingUri', 'category', 'operator', 'spokenLanguages', 'defaultLocale', ]; class FetcherError extends Error {} class FetcherRemoteError extends Error {} class Fetcher { constructor (options) { this.config = Object.assign({}, { timeout: 1000, limit: 10, }, options); } async _getSingleUrl (url, success) { try { const response = await request({ method: 'GET', uri: url, json: true, simple: false, resolveWithFullResponse: true, timeout: this.config.timeout, }); if (response.statusCode > 299) { throw new FetcherRemoteError(`${url} responded with ${response.statusCode}.`); } return success(response); } catch (e) { throw new FetcherRemoteError(e.message); } } async _appendNextPage (options) { const nextItems = await this._fetchHotelAddresses(Object.assign({ url: options.previousResult.next, }, options)); return { addresses: options.previousResult.addresses.concat(nextItems.addresses), errors: options.previousResult.errors ? options.previousResult.errors.concat(nextItems.errors) : nextItems.errors, next: nextItems.next, }; } _fetchHotelAddresses (options) { const expectedUrl = new RegExp(`^${readApiUrl}/hotels`, 'i'); if (options.url && !options.url.match(expectedUrl)) { throw new FetcherError(`${options.url} does not look like hotels list URI`); } return this._getSingleUrl(options.url, (response) => { if (!response.body || !response.body.items) { throw new FetcherRemoteError(`${options.url} did not respond with items list as expected.`); } const items = response.body.items, mappedItems = items.map((a) => a.id), mappedErrors = response.body.errors ? response.body.errors.map((a) => a.data && a.data.id).filter(f => !!f) : [], mappedWarnings = response.body.warnings ? response.body.warnings.map((a) => a.data && a.data.id).filter(f => !!f) : [], result = { addresses: mappedItems, errors: mappedErrors.concat(mappedWarnings), next: response.body.next, }; if (options.onEveryPage) { options.onEveryPage(result); } if (response.body.next && (!options.maxPages || (options.maxPages && options.counter < options.maxPages))) { return this._appendNextPage({ maxPages: options.maxPages, counter: ++options.counter, previousResult: result, onEveryPage: options.onEveryPage, }); } return result; }); } _fetchHotelResource (hotelAddress, url) { if (!hotelAddress) { throw new FetcherError('hotelAddress is required'); } return this._getSingleUrl(url, (response) => { // eslint-disable-next-line no-prototype-builtins if (response.headers.hasOwnProperty('x-data-validation-warning')) { // Don't show data with warnings for now throw new FetcherError('upstream data validation failed'); } return response.body; }); } fetchHotelList (options = {}) { return this._fetchHotelAddresses(Object.assign({}, { counter: 1, url: `${readApiUrl}/hotels?limit=${this.config.limit}&fields=id`, }, options)); }; fetchMeta (hotelAddress) { return this._fetchHotelResource(hotelAddress, `${readApiUrl}/hotels/${hotelAddress}/meta`); }; fetchDescription (hotelAddress) { return this._fetchHotelResource(hotelAddress, `${readApiUrl}/hotels/${hotelAddress}?fields=${DESCRIPTION_FIELDS.join(',')}`); }; fetchRatePlans (hotelAddress) { return this._fetchHotelResource(hotelAddress, `${readApiUrl}/hotels/${hotelAddress}/ratePlans`).then(body => body.items); }; fetchAvailability (hotelAddress) { return this._fetchHotelResource(hotelAddress, `${readApiUrl}/hotels/${hotelAddress}/availability`).then(body => body.items); }; } module.exports = { DESCRIPTION_FIELDS, Fetcher, FetcherError, FetcherRemoteError, };