@mapbox/mapbox-sdk
Version:
JS SDK for accessing Mapbox APIs
263 lines (239 loc) • 8.58 kB
JavaScript
'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;