UNPKG

@mapbox/mapbox-sdk

Version:
263 lines (239 loc) 8.58 kB
'use strict'; var parseToken = require('@mapbox/parse-mapbox-token'); var xtend = require('xtend'); var EventEmitter = require('eventemitter3'); var urlUtils = require('../helpers/url-utils'); var constants = require('../constants'); var requestId = 1; /** * A Mapbox API request. * * Note that creating a `MapiRequest` does *not* send the request automatically. * Use the request's `send` method to send it off and get a `Promise`. * * The `emitter` property is an `EventEmitter` that emits the following events: * * - `'response'` - Listeners will be called with a `MapiResponse`. * - `'error'` - Listeners will be called with a `MapiError`. * - `'downloadProgress'` - Listeners will be called with `ProgressEvents`. * - `'uploadProgress'` - Listeners will be called with `ProgressEvents`. * Upload events are only available when the request includes a file. * * @class MapiRequest * @property {EventEmitter} emitter - An event emitter. See above. * @property {MapiClient} client - This request's `MapiClient`. * @property {MapiResponse|null} response - If this request has been sent and received * a response, the response is available on this property. * @property {MapiError|Error|null} error - If this request has been sent and * received an error in response, the error is available on this property. * @property {boolean} aborted - If the request has been aborted * (via [`abort`](#abort)), this property will be `true`. * @property {boolean} sent - If the request has been sent, this property will * be `true`. You cannot send the same request twice, so if you need to create * a new request that is the equivalent of an existing one, use * [`clone`](#clone). * @property {string} path - The request's path, including colon-prefixed route * parameters. * @property {string} origin - The request's origin. * @property {string} method - The request's HTTP method. * @property {Object} query - A query object, which will be transformed into * a URL query string. * @property {Object} params - A route parameters object, whose values will * be interpolated the path. * @property {Object} headers - The request's headers. * @property {Object|string|null} body - Data to send with the request. * If the request has a body, it will also be sent with the header * `'Content-Type: application/json'`. * @property {Blob|ArrayBuffer|string|ReadStream} file - A file to * send with the request. The browser client accepts Blobs and ArrayBuffers; * the Node client accepts strings (filepaths) and ReadStreams. * @property {string} encoding - The encoding of the response. * @property {string} sendFileAs - The method to send the `file`. Options are * `data` (x-www-form-urlencoded) or `form` (multipart/form-data). */ /** * @ignore * @param {MapiClient} client * @param {Object} options * @param {string} options.method * @param {string} options.path * @param {Object} [options.query={}] * @param {Object} [options.params={}] * @param {string} [options.origin] * @param {Object} [options.headers] * @param {Object} [options.body=null] * @param {Blob|ArrayBuffer|string|ReadStream} [options.file=null] * @param {string} [options.encoding=utf8] */ function MapiRequest(client, options) { if (!client) { throw new Error('MapiRequest requires a client'); } if (!options || !options.path || !options.method) { throw new Error( 'MapiRequest requires an options object with path and method properties' ); } var defaultHeaders = {}; if (options.body) { defaultHeaders['content-type'] = 'application/json'; } var headersWithDefaults = xtend(defaultHeaders, options.headers); // Disallows duplicate header names of mixed case, // e.g. Content-Type and content-type. var headers = Object.keys(headersWithDefaults).reduce(function(memo, name) { memo[name.toLowerCase()] = headersWithDefaults[name]; return memo; }, {}); this.id = requestId++; this._options = options; this.emitter = new EventEmitter(); this.client = client; this.response = null; this.error = null; this.sent = false; this.aborted = false; this.path = options.path; this.method = options.method; this.origin = options.origin || client.origin; this.query = options.query || {}; this.params = options.params || {}; this.body = options.body || null; this.file = options.file || null; this.encoding = options.encoding || 'utf8'; this.sendFileAs = options.sendFileAs || null; this.headers = headers; } /** * Get the URL of the request. * * @param {string} [accessToken] - By default, the access token of the request's * client is used. * @return {string} */ MapiRequest.prototype.url = function url(accessToken) { var url = urlUtils.prependOrigin(this.path, this.origin); url = urlUtils.appendQueryObject(url, this.query); var routeParams = this.params; var actualAccessToken = accessToken == null ? this.client.accessToken : accessToken; if (actualAccessToken) { url = urlUtils.appendQueryParam(url, 'access_token', actualAccessToken); var accessTokenOwnerId = parseToken(actualAccessToken).user; routeParams = xtend({ ownerId: accessTokenOwnerId }, routeParams); } url = urlUtils.interpolateRouteParams(url, routeParams); return url; }; /** * Send the request. Returns a Promise that resolves with a `MapiResponse`. * You probably want to use `response.body`. * * `send` only retrieves the first page of paginated results. You can get * the next page by using the `MapiResponse`'s [`nextPage`](#nextpage) * function, or iterate through all pages using [`eachPage`](#eachpage) * instead of `send`. * * @returns {Promise<MapiResponse>} */ MapiRequest.prototype.send = function send() { var self = this; if (self.sent) { throw new Error( 'This request has already been sent. Check the response and error properties. Create a new request with clone().' ); } self.sent = true; return self.client.sendRequest(self).then( function(response) { self.response = response; self.emitter.emit(constants.EVENT_RESPONSE, response); return response; }, function(error) { self.error = error; self.emitter.emit(constants.EVENT_ERROR, error); throw error; } ); }; /** * Abort the request. * * Any pending `Promise` returned by [`send`](#send) will be rejected with * an error with `type: 'RequestAbortedError'`. If you've created a request * that might be aborted, you need to catch and handle such errors. * * This method will also abort any requests created while fetching subsequent * pages via [`eachPage`](#eachpage). * * If the request has not been sent or has already been aborted, nothing * will happen. */ MapiRequest.prototype.abort = function abort() { if (this._nextPageRequest) { this._nextPageRequest.abort(); delete this._nextPageRequest; } if (this.response || this.error || this.aborted) return; this.aborted = true; this.client.abortRequest(this); }; /** * Invoke a callback for each page of a paginated API response. * * The callback should have the following signature: * * ```js * ( * error: MapiError, * response: MapiResponse, * next: () => void * ) => void * ``` * * **The next page will not be fetched until you've invoked the * `next` callback**, indicating that you're ready for it. * * @param {Function} callback */ MapiRequest.prototype.eachPage = function eachPage(callback) { var self = this; function handleResponse(response) { function getNextPage() { delete self._nextPageRequest; var nextPageRequest = response.nextPage(); if (nextPageRequest) { self._nextPageRequest = nextPageRequest; getPage(nextPageRequest); } } callback(null, response, getNextPage); } function handleError(error) { callback(error, null, function() {}); } function getPage(request) { request.send().then(handleResponse, handleError); } getPage(this); }; /** * Clone this request. * * Each request can only be sent *once*. So if you'd like to send the * same request again, clone it and send away. * * @returns {MapiRequest} - A new `MapiRequest` configured just like this one. */ MapiRequest.prototype.clone = function clone() { return this._extend(); }; /** * @ignore */ MapiRequest.prototype._extend = function _extend(options) { var extendedOptions = xtend(this._options, options); return new MapiRequest(this.client, extendedOptions); }; module.exports = MapiRequest;