├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── bundle.js ├── conf ├── classes-documentation.yml └── service-documentation.yml ├── docs ├── classes.md ├── development.md └── services.md ├── index.js ├── lib ├── browser │ ├── .eslintrc.json │ ├── __tests__ │ │ └── browser-layer.test.js │ ├── browser-client.js │ └── browser-layer.js ├── classes │ ├── __tests__ │ │ ├── mapi-client.test.js │ │ ├── mapi-error.test.js │ │ ├── mapi-request.test.js │ │ └── mapi-response.test.js │ ├── mapi-client.js │ ├── mapi-error.js │ ├── mapi-request.js │ └── mapi-response.js ├── client.js ├── constants.js ├── helpers │ ├── __tests__ │ │ ├── parse-headers.test.js │ │ ├── parse-link-header.test.js │ │ └── url-utils.test.js │ ├── parse-headers.js │ ├── parse-link-header.js │ └── url-utils.js └── node │ ├── .eslintrc.json │ ├── node-client.js │ └── node-layer.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── services ├── __tests__ │ ├── datasets.test.js │ ├── directions.test.js │ ├── geocoding-v6.test.js │ ├── geocoding.test.js │ ├── isochrone.test.js │ ├── map-matching.test.js │ ├── matrix.test.js │ ├── optimization.test.js │ ├── static.test.js │ ├── styles.test.js │ ├── tilequery.test.js │ ├── tilesets.test.js │ ├── tokens.test.js │ └── uploads.test.js ├── datasets.js ├── directions.js ├── geocoding-v6.js ├── geocoding.js ├── isochrone.js ├── map-matching.js ├── matrix.js ├── optimization.js ├── service-helpers │ ├── __tests__ │ │ ├── fixtures │ │ │ └── foo.txt │ │ ├── validator-browser.test.js │ │ └── validator.test.js │ ├── create-service-factory.js │ ├── generic-types.js │ ├── object-clean.js │ ├── object-map.js │ ├── pick.js │ ├── stringify-booleans.js │ └── validator.js ├── static.js ├── styles.js ├── tilequery.js ├── tilesets.js ├── tokens.js └── uploads.js └── test ├── browser-interface.test.js ├── bundle.test.js ├── node-interface.test.js ├── test-shared-interface.js ├── test-utils.js ├── try-browser ├── .eslintrc.json ├── index.html └── try-browser.js └── try-node.js /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | umd 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "plugins": ["node"], 4 | "env": { 5 | "commonjs": true, 6 | "es6": false 7 | }, 8 | "globals": { 9 | "Promise": true 10 | }, 11 | "parserOptions": { 12 | "sourceType": "script" 13 | }, 14 | "rules": { 15 | "strict": ["error"], 16 | "eqeqeq": ["error", "smart"], 17 | "node/no-unsupported-features": ["error"], 18 | "node/no-missing-require": "error" 19 | }, 20 | "overrides": [ 21 | { 22 | "files": ["test/*.js", "**/__tests__/*.js"], 23 | "env": { 24 | "jest": true, 25 | "es6": true, 26 | "node": true 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | umd 4 | .DS_Store 5 | .nyc_output 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .eslintignore -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: focal 2 | language: node_js 3 | node_js: 4 | - lts/* 5 | cache: 6 | directories: 7 | - node_modules 8 | script: 9 | - npm run test 10 | - npm run bundle 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.16.1 4 | 5 | - **Add:** Add `session_token` as optional parameter in the geocoding services 6 | 7 | ## 0.16.0 8 | 9 | - **Add:** Add `depart_at` as optional parameter in the isochrone service 10 | 11 | ## 0.15.6 12 | 13 | - **Fix:** Add `secondary_address` to list of accepted types for Geocoding v6 14 | 15 | ## 0.15.5 16 | 17 | - **Fix:** Rename directions api annotation option `max_speed` to `maxspeed` [#480](https://github.com/mapbox/mapbox-sdk-js/pull/480) 18 | 19 | ## 0.15.4 20 | 21 | - **Fix:** Set `body = ''` for POST, PUT, PATCH, and DELETE requests that do not have a body [#479](https://github.com/mapbox/mapbox-sdk-js/pull/479) 22 | 23 | ## 0.15.3 24 | 25 | - **Fix:** Maximum Optimization V1 request size limits should not be enforced client-side 26 | 27 | ## 0.15.2 28 | 29 | - **Add:** Add `depart_at` and `arrive_by` as optional parameters in the directions service 30 | 31 | ## 0.15.1 32 | 33 | - **Add:** Add `driving-traffic` as a profile option in the isochrone service 34 | 35 | ## 0.15.0 36 | 37 | - **Add:** Geocoding `v6` service 38 | 39 | ## 0.14.0 40 | 41 | - **Add:** Config for `Tokens#createToken` and `Tokens#updateToken` can now include the `allowedApplications` property. 42 | 43 | 44 | ## 0.13.7 45 | 46 | - **Add:** add additional EV values to the directions API request 47 | 48 | ## 0.13.6 49 | 50 | - **Add:** add additional valid values for the directions api `annotations` option 51 | - **Chore:** update `documentation`, `budo`, `meow`, and `remark-preset-davidtheclark` dependencies 52 | - **Chore:** update `jest` and associated dependencies 53 | 54 | ## 0.13.5 55 | 56 | - **Revert:** add `driving-traffic` profile to Isochrone service. 57 | - **Fix:** Pin CI node version to LTS release (current LTS is 16.x). 58 | - **Fix:** Fix-up package-lock.json with new metadata (based on node 14). 59 | - **Fix:** Update `got` dependency to 11.8.5 (Fix for CVE-2022-33987). 60 | - **Add:** add electric vehicle routing parameters to Directions service. 61 | 62 | ## 0.13.4 63 | 64 | **Add:** add `contours_meters` Isochrone API parameter 65 | 66 | ## 0.13.3 67 | 68 | **Add:** add `ip` as valid value for `proximity` parameter in the geocoding service 69 | 70 | ## 0.13.2 71 | 72 | **Add:** add `fuzzyMatch` and `worldview` parameters to the geocoding service 73 | 74 | ## 0.13.1 75 | 76 | **Fix:** Update `got` depdendency to 10.7.0 77 | 78 | ## 0.13.0 79 | 80 | **Add:** add `driving-traffic` profile to Isochrone service. 81 | 82 | ## 0.12.1 83 | 84 | - **PATCH:** [remove unsupported `private` option](https://github.com/mapbox/mapbox-sdk-js/pull/405) from `createUpload`. 85 | 86 | ## 0.12.0 87 | 88 | - **Add:** add `bounding box` parameter as a position option for `Static#getStaticImage.` 89 | - **Add:** add `padding` optional parameter for `Static#getStaticImage`. 90 | 91 | ## 0.11.0 92 | 93 | - **Add:** add `fresh` parameter to `Styles#getStyle` to bypass the cached version of the style. 94 | - **Add:** add `routing` parameter to `Geocoding#forwardGeocode` and `Geocoding#reverseGeocoding`. 95 | - **Add:** add `driving-traffic` profile to `Optimization#getOptimization`. 96 | - **Add:** add `sortby` parameter to `Datasets#listDatasets`. 97 | - **Add:** add `Tilesets#updateTileset`. 98 | - **Add:** add `fallback`, `mapboxGLVersion` and `mapboxGLGeocoderVersion` to `Styles#getEmbeddableHtml`. 99 | - **Add:** add pagination support to `Tilesets#listTilesetJobs` and `Tilesets#listTilesetSources`. 100 | - **Breaking change:** `Uploads#createUpload`'s `mapId` parameter is now `tileset`, and `tilesetName` is now `name` to be consistent across the API. `mapId` and `tilesetName` are deprecated, but will still work and may be removed in a future release. 101 | - **Add:** add `private` option to `Uploads#createUpload`. 102 | - **Fix:** fixed an issue where array parameters containing falsy values (e.g. for the `proximity` parameter in `forwardGeocode`, where longitude or latitude coordinates are 0) were not being applied correctly. 103 | 104 | ## 0.10.0 105 | 106 | - **Add:** add new parameters to `Tilesets#listTilesets`: `type`, `limit`, `sortBy`, `start` and `visibility`. 107 | - **Add:** add `Tilesets#tileJSONMetadata` method to retrieve a Tileset TileJSON metadata. 108 | - **Add:** add new `metadata` parameter to `Styles#getStyle` to preserve `mapbox:` specific metadata from the style. 109 | - **Add:** add new Tilesets methods `deleteTileset`, `createTilesetSource`, `getTilesetSource`, `listTilesetSources`, `deleteTilesetSource`, `createTileset`, `publishTileset`, `tilesetStatus`, `tilesetJob`, `listTilesetJobs`, `getTilesetsQueue`, `validateRecipe`, `getRecipe`, `updateRecipe`. 110 | - **Add:** add new `draft` parameter to `Styles#getStyle`, `Styles#deleteStyleIcon` and `Styles#getStyleSprite`, `Styles#getEmbeddableHtml` to work with draft styles. 111 | - **Fix:** Fix responses containing binary data when using `Static#getStaticImage`, `Styles#getStyleSprite` and `Styles#getFontGlyphRange`. 112 | - **Fix:** Fix requests for highres sprites in `Styles#getStyleSprite`. 113 | - **Fix:** set `position.bearing` to `0` if `position.pitch` is defined and `position.bearing` is not in the Static API. 114 | - **Fix:** use `tilesets.getRecipe` in tilesets API example. 115 | 116 | ## 0.9.0 117 | 118 | - **Add:** add Isochrone API service. 119 | 120 | ## 0.8.0 121 | 122 | - **Add**: add new style parameters to the Static Images API service: `addlayer`, `setfilter`, and `layer_id`. 123 | - **Breaking change**: `insertOverlayBeforeLayer` is now `before_layer` in the Static Images API service. This change uses the API's name for the field and to support that the field can be used with the new style parameter `addlayer`. 124 | 125 | ## 0.7.1 126 | 127 | - **Fix:** add missing `geometry` key to Tilequery service. 128 | 129 | ## 0.7.0 130 | 131 | - **Fix:** filter empty waypoints from map matching requests. 132 | - **Fix:** fix url token placement for service with clients. 133 | 134 | ## 0.6.0 135 | 136 | - **Fix:** `Tokens#updateToken` can now set `null` value to `referrers` property to delete the property. 137 | - **Fix:** `Tokens#updateToken` can now set `null` value to `resources` property to delete the property. 138 | - **Breaking change**: change all references to `referrer`{s} to `allowedUrl`{s}. 139 | 140 | ## 0.5.0 141 | 142 | - **Add:** Config for `Tokens#createToken` and `Tokens#updateToken` can now include the `referrers` property. 143 | 144 | ## 0.4.1 145 | 146 | - **Fix:** Fix a CORS-related bug that caused Firefox to send preflight `OPTIONS` requests that were rejected by the server. This bug surfaced in Firefox with the Tilequery API, but may possibly have affected some other endpoints and some other browsers. 147 | 148 | ## 0.4.0 149 | 150 | - **Breaking change & fix:** Config for `Static#getStaticImage` now includes a `position` property that can be either `"auto"` or an object with `coordinates`, `zoom`, etc., properties. This fixes buggy behavior with the `"auto"` keyboard by forcing it to be mutually exclusive with all other positioning options. 151 | - **Fix:** Fix bug in `Static#getStaticImage` that resulted in reversed coordinates when creating a polyline overlay. 152 | - **Fix:** Loosen the type validation on coordinates, since longitudes greater than 180 or less than -180 are valid for some APIs. 153 | 154 | ## 0.3.0 155 | 156 | - **Change:** Rename `MapMatching#getMatching` to `MapMatching#getMatch`. 157 | - **Change:** Throw validation error if request configuration object includes invalid properties. This is a breaking change because it could cause your code to throw a new validation error informing you of a mistake in your code. But there is no change to the library's functionality: you'll just need to clean up invalid properties. 158 | 159 | ## 0.2.0 160 | 161 | - **Add:** Add Optimization API service. 162 | 163 | ## 0.1.3 164 | 165 | - **Fix:** Include all services in the UMD bundled client. Several were missing. 166 | 167 | ## 0.1.2 168 | 169 | - **Chore:** Use `@mapbox/fusspot` for validation; remove the local implementation. 170 | 171 | ## 0.1.1 172 | 173 | - **Fix:** `Directions#getDirections` and `Geocoding#forwardGeocode` stringify boolean query parameters like `steps` and `autocomplete`. 174 | 175 | ## 0.1.0 176 | 177 | - Brand new codebase. Please read the documentation and try it out! The `mapbox` npm package is now deprecated in favor of the new `@mapbox/mapbox-sdk`. 178 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. 6 | 7 | Examples of unacceptable behavior by participants include: 8 | 9 | - The use of sexualized language or imagery 10 | - Personal attacks 11 | - Trolling or insulting/derogatory comments 12 | - Public or private harassment 13 | - Publishing other's private information, such as physical or electronic addresses, without explicit permission 14 | - Other unethical or unprofessional conduct 15 | 16 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. 17 | 18 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 19 | 20 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 21 | 22 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, available at 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Mapbox 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | The views and conclusions contained in the software and documentation are those 25 | of the authors and should not be interpreted as representing official policies, 26 | either expressed or implied, of the FreeBSD Project. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @mapbox/mapbox-sdk 2 | 3 | [![Build Status](https://travis-ci.com/mapbox/mapbox-sdk-js.svg?branch=main)](https://travis-ci.com/mapbox/mapbox-sdk-js) 4 | 5 | A JS SDK for working with [Mapbox APIs](https://docs.mapbox.com/api/). 6 | 7 | Works in Node, the browser, and React Native. 8 | 9 | **As of 6/11/18, the codebase has been rewritten and a new npm package released.** 10 | The `mapbox` package is deprecated in favor of the new `@mapbox/mapbox-sdk` package. 11 | Please read the documentation and open issues with questions or problems. 12 | 13 | ## Table of contents 14 | 15 | - [Installation](#installation) 16 | - [Usage](#usage) 17 | - [Creating clients](#creating-clients) 18 | - [Creating and sending requests](#creating-and-sending-requests) 19 | - [Overview of requests, responses, and errors](#overview-of-requests-responses-and-errors) 20 | - [MapiRequest](#mapirequest) 21 | - [MapiResponse](#mapiresponse) 22 | - [MapiError](#mapierror) 23 | - [Services](#services) 24 | - [Pre-bundled files on unpkg.com](#pre-bundled-files-on-unpkgcom) 25 | - [Development](#development) 26 | 27 | ## Installation 28 | 29 | ``` 30 | npm install @mapbox/mapbox-sdk 31 | ``` 32 | 33 | **If you are supporting older browsers, you will need a Promise polyfill.** 34 | [es6-promise](https://github.com/stefanpenner/es6-promise) is a good one, if you're uncertain. 35 | 36 | The documentation below assumes you're using a JS module system. 37 | If you aren't, read ["Pre-bundled files on unpkg.com"](#pre-bundled-files-on-unpkgcom). 38 | 39 | ## Usage 40 | 41 | There are 3 basic steps to getting an API response: 42 | 43 | 1. Create a client. 44 | 2. Create a request. 45 | 3. Send the request. 46 | 47 | ### Creating clients 48 | 49 | To **create a service client**, import the service's factory function from `'@mapbox/mapbox-sdk/services/{service}'` and provide it with your access token. 50 | 51 | The service client exposes methods that create requests. 52 | 53 | ```js 54 | const mbxStyles = require('@mapbox/mapbox-sdk/services/styles'); 55 | const stylesService = mbxStyles({ accessToken: MY_ACCESS_TOKEN }); 56 | // stylesService exposes listStyles(), createStyle(), getStyle(), etc. 57 | ``` 58 | 59 | You can also **share one configuration between multiple services**. 60 | To do that, initialize a base client and then pass *that* into service factory functions. 61 | 62 | ```js 63 | const mbxClient = require('@mapbox/mapbox-sdk'); 64 | const mbxStyles = require('@mapbox/mapbox-sdk/services/styles'); 65 | const mbxTilesets = require('@mapbox/mapbox-sdk/services/tilesets'); 66 | 67 | const baseClient = mbxClient({ accessToken: MY_ACCESS_TOKEN }); 68 | const stylesService = mbxStyles(baseClient); 69 | const tilesetsService = mbxTilesets(baseClient); 70 | ``` 71 | 72 | ### Creating and sending requests 73 | 74 | To **create a request**, invoke a method on a service client. 75 | 76 | Once you've created a request, **send the request** with its `send` method. 77 | It will return a Promise that resolves with a `MapiResponse`. 78 | 79 | ```js 80 | const mbxClient = require('@mapbox/mapbox-sdk'); 81 | const mbxStyles = require('@mapbox/mapbox-sdk/services/styles'); 82 | const mbxTilesets = require('@mapbox/mapbox-sdk/services/tilesets'); 83 | 84 | const baseClient = mbxClient({ accessToken: MY_ACCESS_TOKEN }); 85 | const stylesService = mbxStyles(baseClient); 86 | const tilesetsService = mbxTilesets(baseClient); 87 | 88 | // Create a style. 89 | stylesService.createStyle({..}) 90 | .send() 91 | .then(response => {..}, error => {..}); 92 | 93 | // List tilesets. 94 | tilesetsService.listTilesets() 95 | .send() 96 | .then(response => {..}, error => {..}) 97 | ``` 98 | 99 | ## Overview of requests, responses, and errors 100 | 101 | **For more details, please read [the full classes documentation](./docs/classes.md).** 102 | 103 | ### `MapiRequest` 104 | 105 | Service methods return `MapiRequest` objects. 106 | 107 | Typically, you'll create a `MapiRequest` then `send` it. 108 | `send` returns a `Promise` that resolves with a [`MapiResponse`] or rejects with a [`MapiError`]. 109 | 110 | `MapiRequest`s also expose other properties and methods that you might use from time to time. 111 | For example: 112 | 113 | - `MapiRequest#abort` aborts the request. 114 | - `MapiRequest#eachPage` executes a callback for each page of a paginated API response. 115 | - `MapiRequest.emitter` exposes an event emitter that fires events like `downloadProgress` and `uploadProgress`. 116 | 117 | For more details, please read [the full `MapiRequest` documentation](./docs/classes.md#mapirequest). 118 | 119 | ```js 120 | // Create a request and send it. 121 | stylesService.createStyle({..}) 122 | .send() 123 | .then(response => {..}, error => {..}); 124 | 125 | // Abort a request. 126 | const req = tilesetsService.listTilesets(); 127 | req.send().then(response => {..}, error => { 128 | // Because the request is aborted, an error will be thrown that we can 129 | // catch and handle. 130 | }); 131 | req.abort(); 132 | 133 | // Paginate through a response. 134 | tilesetsService.listTilesets().eachPage((error, response, next) => { 135 | // Do something with the page, then call next() to send the request 136 | // for the next page. 137 | 138 | // You can check whether there will be a next page using 139 | // MapiResponse#hasNextPage, if you want to do something 140 | // different on the last page. 141 | if (!response.hasNextPage()) {..} 142 | }); 143 | 144 | // Listen for uploadProgress events. 145 | const req = stylesService.createStyleIcon({..}); 146 | req.on('uploadProgress', event => { 147 | // Do something with the progress event information. 148 | }); 149 | req.send().then(response => {..}, error => {..}); 150 | ``` 151 | 152 | ### `MapiResponse` 153 | 154 | When you `send` a [`MapiRequest`], the returned `Promise` resolves with a `MapiResponse`. 155 | 156 | Typically, you'll use `MapiResponse.body` to access the parsed API response. 157 | 158 | `MapiResponse`s also expose other properties and methods. 159 | For example: 160 | 161 | - `MapiResponse#hasNextPage` indicates if there is another page of results. 162 | - If there is another page, `MapiResponse#nextPage` creates a [`MapiRequest`] that you can `send` to get that next page. 163 | - `MapiResponse.headers` exposes the parsed HTTP headers from the API response. 164 | 165 | For more details, please read [the full `MapiResponse` documentation](./docs/classes.md#mapiresponse). 166 | 167 | ```js 168 | // Read a response body. 169 | stylesService.getStyle({..}) 170 | .send() 171 | .then(resp => { 172 | const style = resp.body; 173 | // Do something with the style. 174 | }, err => {..}); 175 | 176 | // Get the next page of results. 177 | tilesetsService.listTilesets() 178 | .send() 179 | .then(resp => { 180 | if (resp.hasNextPage()) { 181 | const nextPageReq = resp.nextPage(); 182 | nextPageReq.send().then(..); 183 | } 184 | }, err => {..}); 185 | 186 | // Check the headers. 187 | tilesetsService.listTilesets() 188 | .send() 189 | .then(resp => { 190 | console.log(resp.headers); 191 | }, err => {..}); 192 | ``` 193 | 194 | ### `MapiError` 195 | 196 | If the server responds to your [`MapiRequest`] with an error, or if you abort the request, the `Promise` returned by `send` will reject with a `MapiError`. 197 | 198 | `MapiError`s expose the information you'll need to handle and respond to the error. 199 | For example: 200 | 201 | - `MapiError.type` exposes the type of error, so you'll know if it was an HTTP error from the server or the request was aborted. 202 | - `MapiError.statusCode` exposes the status code of HTTP errors. 203 | - `MapiError.body` exposes the body of the HTTP response, parsed as JSON if possible. 204 | - `MapiError.message` tells you what went wrong. 205 | 206 | For more details, please read [the full `MapiError` documentation](./docs/classes.md#mapierror). 207 | 208 | ```js 209 | // Check the error. 210 | stylesService.getStyle({..}) 211 | .send() 212 | .then(response => {..}, error => { 213 | if (err.type === 'RequestAbortedError') { 214 | return; 215 | } 216 | console.error(error.message); 217 | }); 218 | ``` 219 | 220 | ## Services 221 | 222 | Please read [the full documentation for services](./docs/services.md). 223 | 224 | ## Pre-bundled files on unpkg.com 225 | 226 | If you aren't using a JS module system, you can use a ` 230 | 231 | ``` 232 | 233 | These files are a UMD build of the package, exposing a global `mapboxSdk` function that creates a client, initializes *all* the services, and attaches those services to the client. 234 | Here's how you might use it. 235 | 236 | ```html 237 | 238 | 247 | ``` 248 | 249 | ## Development 250 | 251 | Please read [`./docs/development.md`](./docs/development.md). 252 | 253 | [`got`]: https://github.com/sindresorhus/got 254 | 255 | [`http`]: https://nodejs.org/api/http.html 256 | 257 | [`xmlhttprequest`]: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest 258 | 259 | [`mapirequest`]: #mapirequest 260 | 261 | [`mapiresponse`]: #mapiresponse 262 | 263 | [`mapierror`]: #mapierror 264 | -------------------------------------------------------------------------------- /bundle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var browserClient = require('./lib/browser/browser-client'); 4 | var mbxDatasets = require('./services/datasets'); 5 | var mbxDirections = require('./services/directions'); 6 | var mbxGeocoding = require('./services/geocoding'); 7 | var mbxGeocodingV6 = require('./services/geocoding-v6'); 8 | var mbxMapMatching = require('./services/map-matching'); 9 | var mbxMatrix = require('./services/matrix'); 10 | var mbxOptimization = require('./services/optimization'); 11 | var mbxStatic = require('./services/static'); 12 | var mbxStyles = require('./services/styles'); 13 | var mbxTilequery = require('./services/tilequery'); 14 | var mbxTilesets = require('./services/tilesets'); 15 | var mbxTokens = require('./services/tokens'); 16 | var mbxUploads = require('./services/uploads'); 17 | var mbxIsochrone = require('./services/isochrone'); 18 | 19 | function mapboxSdk(options) { 20 | var client = browserClient(options); 21 | 22 | client.datasets = mbxDatasets(client); 23 | client.directions = mbxDirections(client); 24 | client.geocoding = mbxGeocoding(client); 25 | client.geocodingV6 = mbxGeocodingV6(client); 26 | client.mapMatching = mbxMapMatching(client); 27 | client.matrix = mbxMatrix(client); 28 | client.optimization = mbxOptimization(client); 29 | client.static = mbxStatic(client); 30 | client.styles = mbxStyles(client); 31 | client.tilequery = mbxTilequery(client); 32 | client.tilesets = mbxTilesets(client); 33 | client.tokens = mbxTokens(client); 34 | client.uploads = mbxUploads(client); 35 | client.isochrone = mbxIsochrone(client); 36 | 37 | return client; 38 | } 39 | 40 | module.exports = mapboxSdk; 41 | -------------------------------------------------------------------------------- /conf/classes-documentation.yml: -------------------------------------------------------------------------------- 1 | toc: 2 | - MapiRequest 3 | - MapiResponse 4 | - MapiError 5 | - MapiClient 6 | -------------------------------------------------------------------------------- /conf/service-documentation.yml: -------------------------------------------------------------------------------- 1 | toc: 2 | - Styles 3 | - Static 4 | - Uploads 5 | - Datasets 6 | - Tilequery 7 | - Tilesets 8 | - Geocoding 9 | - Directions 10 | - MapMatching 11 | - Matrix 12 | - Optimization 13 | - Tokens 14 | - name: Data structures 15 | description: | 16 | Data structures used in service method configuration. 17 | children: 18 | - DirectionsWaypoint 19 | - MapMatchingPoint 20 | - MatrixPoint 21 | - OptimizationWaypoint 22 | - SimpleMarkerOverlay 23 | - CustomMarkerOverlay 24 | - PathOverlay 25 | - GeoJsonOverlay 26 | - UploadableFile 27 | - Coordinates 28 | - BoundingBox 29 | -------------------------------------------------------------------------------- /docs/classes.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Table of Contents 4 | 5 | * [MapiRequest][1] 6 | * [Properties][2] 7 | * [url][3] 8 | * [Parameters][4] 9 | * [send][5] 10 | * [abort][6] 11 | * [eachPage][7] 12 | * [Parameters][8] 13 | * [clone][9] 14 | * [MapiResponse][10] 15 | * [Properties][11] 16 | * [hasNextPage][12] 17 | * [nextPage][13] 18 | * [MapiError][14] 19 | * [Properties][15] 20 | * [MapiClient][16] 21 | * [Properties][17] 22 | 23 | ## MapiRequest 24 | 25 | A Mapbox API request. 26 | 27 | Note that creating a `MapiRequest` does *not* send the request automatically. 28 | Use the request's `send` method to send it off and get a `Promise`. 29 | 30 | The `emitter` property is an `EventEmitter` that emits the following events: 31 | 32 | * `'response'` - Listeners will be called with a `MapiResponse`. 33 | * `'error'` - Listeners will be called with a `MapiError`. 34 | * `'downloadProgress'` - Listeners will be called with `ProgressEvents`. 35 | * `'uploadProgress'` - Listeners will be called with `ProgressEvents`. 36 | Upload events are only available when the request includes a file. 37 | 38 | ### Properties 39 | 40 | * `emitter` **EventEmitter** An event emitter. See above. 41 | * `client` **[MapiClient][16]** This request's `MapiClient`. 42 | * `response` **([MapiResponse][10] | null)** If this request has been sent and received 43 | a response, the response is available on this property. 44 | * `error` **([MapiError][14] | [Error][18] | null)** If this request has been sent and 45 | received an error in response, the error is available on this property. 46 | * `aborted` **[boolean][19]** If the request has been aborted 47 | (via [`abort`][6]), this property will be `true`. 48 | * `sent` **[boolean][19]** If the request has been sent, this property will 49 | be `true`. You cannot send the same request twice, so if you need to create 50 | a new request that is the equivalent of an existing one, use 51 | [`clone`][9]. 52 | * `path` **[string][20]** The request's path, including colon-prefixed route 53 | parameters. 54 | * `origin` **[string][20]** The request's origin. 55 | * `method` **[string][20]** The request's HTTP method. 56 | * `query` **[Object][21]** A query object, which will be transformed into 57 | a URL query string. 58 | * `params` **[Object][21]** A route parameters object, whose values will 59 | be interpolated the path. 60 | * `headers` **[Object][21]** The request's headers. 61 | * `body` **([Object][21] | [string][20] | null)** Data to send with the request. 62 | If the request has a body, it will also be sent with the header 63 | `'Content-Type: application/json'`. 64 | * `file` **([Blob][22] | [ArrayBuffer][23] | [string][20] | ReadStream)** A file to 65 | send with the request. The browser client accepts Blobs and ArrayBuffers; 66 | the Node client accepts strings (filepaths) and ReadStreams. 67 | * `encoding` **[string][20]** The encoding of the response. 68 | * `sendFileAs` **[string][20]** The method to send the `file`. Options are 69 | `data` (x-www-form-urlencoded) or `form` (multipart/form-data). 70 | 71 | ### url 72 | 73 | Get the URL of the request. 74 | 75 | #### Parameters 76 | 77 | * `accessToken` **[string][20]?** By default, the access token of the request's 78 | client is used. 79 | 80 | Returns **[string][20]** 81 | 82 | ### send 83 | 84 | Send the request. Returns a Promise that resolves with a `MapiResponse`. 85 | You probably want to use `response.body`. 86 | 87 | `send` only retrieves the first page of paginated results. You can get 88 | the next page by using the `MapiResponse`'s [`nextPage`][13] 89 | function, or iterate through all pages using [`eachPage`][7] 90 | instead of `send`. 91 | 92 | Returns **[Promise][24]<[MapiResponse][10]>** 93 | 94 | ### abort 95 | 96 | Abort the request. 97 | 98 | Any pending `Promise` returned by [`send`][5] will be rejected with 99 | an error with `type: 'RequestAbortedError'`. If you've created a request 100 | that might be aborted, you need to catch and handle such errors. 101 | 102 | This method will also abort any requests created while fetching subsequent 103 | pages via [`eachPage`][7]. 104 | 105 | If the request has not been sent or has already been aborted, nothing 106 | will happen. 107 | 108 | ### eachPage 109 | 110 | Invoke a callback for each page of a paginated API response. 111 | 112 | The callback should have the following signature: 113 | 114 | ```js 115 | ( 116 | error: MapiError, 117 | response: MapiResponse, 118 | next: () => void 119 | ) => void 120 | ``` 121 | 122 | **The next page will not be fetched until you've invoked the 123 | `next` callback**, indicating that you're ready for it. 124 | 125 | #### Parameters 126 | 127 | * `callback` **[Function][25]** 128 | 129 | ### clone 130 | 131 | Clone this request. 132 | 133 | Each request can only be sent *once*. So if you'd like to send the 134 | same request again, clone it and send away. 135 | 136 | Returns **[MapiRequest][1]** A new `MapiRequest` configured just like this one. 137 | 138 | ## MapiResponse 139 | 140 | A Mapbox API response. 141 | 142 | ### Properties 143 | 144 | * `body` **[Object][21]** The response body, parsed as JSON. 145 | * `rawBody` **[string][20]** The raw response body. 146 | * `statusCode` **[number][26]** The response's status code. 147 | * `headers` **[Object][21]** The parsed response headers. 148 | * `links` **[Object][21]** The parsed response links. 149 | * `request` **[MapiRequest][1]** The response's originating `MapiRequest`. 150 | 151 | ### hasNextPage 152 | 153 | Check if there is a next page that you can fetch. 154 | 155 | Returns **[boolean][19]** 156 | 157 | ### nextPage 158 | 159 | Create a request for the next page, if there is one. 160 | If there is no next page, returns `null`. 161 | 162 | Returns **([MapiRequest][1] | null)** 163 | 164 | ## MapiError 165 | 166 | A Mapbox API error. 167 | 168 | If there's an error during the API transaction, 169 | the Promise returned by `MapiRequest`'s [`send`][5] 170 | method should reject with a `MapiError`. 171 | 172 | ### Properties 173 | 174 | * `request` **[MapiRequest][1]** The errored request. 175 | * `type` **[string][20]** The type of error. Usually this is `'HttpError'`. 176 | If the request was aborted, so the error was 177 | not sent from the server, the type will be 178 | `'RequestAbortedError'`. 179 | * `statusCode` **[number][26]?** The numeric status code of 180 | the HTTP response. 181 | * `body` **([Object][21] | [string][20])?** If the server sent a response body, 182 | this property exposes that response, parsed as JSON if possible. 183 | * `message` **[string][20]?** Whatever message could be derived from the 184 | call site and HTTP response. 185 | 186 | ## MapiClient 187 | 188 | A low-level Mapbox API client. Use it to create service clients 189 | that share the same configuration. 190 | 191 | Services and `MapiRequest`s use the underlying `MapiClient` to 192 | determine how to create, send, and abort requests in a way 193 | that is appropriate to the configuration and environment 194 | (Node or the browser). 195 | 196 | ### Properties 197 | 198 | * `accessToken` **[string][20]** The Mapbox access token assigned 199 | to this client. 200 | * `origin` **[string][20]?** The origin 201 | to use for API requests. Defaults to [https://api.mapbox.com][27]. 202 | 203 | [1]: #mapirequest 204 | 205 | [2]: #properties 206 | 207 | [3]: #url 208 | 209 | [4]: #parameters 210 | 211 | [5]: #send 212 | 213 | [6]: #abort 214 | 215 | [7]: #eachpage 216 | 217 | [8]: #parameters-1 218 | 219 | [9]: #clone 220 | 221 | [10]: #mapiresponse 222 | 223 | [11]: #properties-1 224 | 225 | [12]: #hasnextpage 226 | 227 | [13]: #nextpage 228 | 229 | [14]: #mapierror 230 | 231 | [15]: #properties-2 232 | 233 | [16]: #mapiclient 234 | 235 | [17]: #properties-3 236 | 237 | [18]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error 238 | 239 | [19]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean 240 | 241 | [20]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String 242 | 243 | [21]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object 244 | 245 | [22]: https://developer.mozilla.org/docs/Web/API/Blob 246 | 247 | [23]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer 248 | 249 | [24]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise 250 | 251 | [25]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function 252 | 253 | [26]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number 254 | 255 | [27]: https://api.mapbox.com 256 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Service method naming conventions 4 | 5 | - Each method name should contain a verb and the object of that verb. 6 | - The following verbs should be used for each method type: 7 | - `get` and `list` for `GET` requests 8 | - `create` for `POST` requests 9 | - `update` for `PATCH` requests 10 | - `put` for `PUT` requests 11 | - `delete` for `DELETE` requests 12 | - Only used special verbs when a clear title can't be constructed from the above verbs. 13 | 14 | ## Test coverage reporting 15 | 16 | To check test coverage, run Jest with the `--coverage` flag. You can do this for a single run (e.g. `npx jest --coverage`) or while watching (e.g. `npx jest --watchAll --coverage`). 17 | 18 | Coverage data is output to the console and written to the `coverage/` directory. You can `open coverage/index.html` to check out the wonderful HTML report and find the lines that still need coverage. 19 | 20 | ## Creating a service 21 | 22 | First you'll create a service prototype object, then you'll pass that prototype object into the [`createServiceFactory()`](../services/service-helpers/create-service-factory.js) function. 23 | 24 | The properties of a service prototype object are the service methods. Each service method is a function that accepts a configuration object and returns a `MapiRequest`. The request should be created using `this.client.createRequest()`, which accepts an object with the following properties: 25 | 26 | - `method` (string): the HTTP method (e.g. `GET`). 27 | - `path` (string): the path of the endpoint, with `:express-style` colon-prefixed parameters for path parts that should be replaced by values in the `params` object. 28 | - `params` (object): an object whose keys correspond to the `:express-style` colon-prefixed parameters in the `path` string, and whose values are the values that should be substituted into the path. For example, with `path: '/foo/:bar/:baz'` you'll need a params object with `bar` and `baz` properties, like `{ bar: 'a', baz: 'b' }`. **You do *not* need to specify an `ownerId` param: that is automatically provided by the `MapiClient`.** 29 | - `query` (object): an object that will be transformed into a query string and attached to the `path` (e.g. `{ foo: 'a', baz: 'b' }` becomes `?foo=a&baz=b`). 30 | - `headers` (object): an object of headers that should be added to the request. Keys should be lowercase. `'content-type': 'application/json'` is automatically included if the request includes a `body`. 31 | - `body` (object, default `null`): a body that should be included with the `POST` or `PUT` request. It will be stringified as JSON. 32 | - `file` (Blob|ArrayBuffer|string|ReadStream): a file that should be included with the `POST` or `PUT` request. 33 | 34 | `createServiceFactory` sets `this.client`, so the service can make requests tailored to the user's `MapiClient`. 35 | 36 | We use [Fusspot](https://github.com/mapbox/fusspot) for run-time validation of service method configuration. 37 | 38 | Here's an example of a minimal fake service: 39 | 40 | ```js 41 | var createServiceFactory = require('../path/to/service-helpers/create-service-factory'); 42 | 43 | var Animals = {}; 44 | 45 | Animals.listAnimals = function() { 46 | return this.client.createRequest({ 47 | method: 'GET', 48 | path: '/animals/v1/:ownerId' 49 | }); 50 | }; 51 | 52 | Animals.deleteAnimal = function(config) { 53 | // Here you can make assertions against config. 54 | 55 | return this.client.createRequest({ 56 | method: 'DELETE', 57 | path: '/animals/v1/:ownerId/:animalId', 58 | params: { animalId: config.animalId } 59 | }); 60 | }; 61 | 62 | var AnimalsService = createServiceFactory(Animals); 63 | ``` 64 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var client = require('./lib/client'); 4 | 5 | module.exports = client; 6 | -------------------------------------------------------------------------------- /lib/browser/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": false 5 | } 6 | } -------------------------------------------------------------------------------- /lib/browser/__tests__/browser-layer.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browserLayer = require('../browser-layer'); 4 | const constants = require('../../constants'); 5 | 6 | describe('sendRequestXhr', () => { 7 | test('upload progress event is not assigned if the request does not include a body or file', () => { 8 | const request = { id: 'fake' }; 9 | const xhr = { 10 | send: jest.fn(), 11 | upload: {}, 12 | getAllResponseHeaders: jest.fn() 13 | }; 14 | 15 | const send = browserLayer.sendRequestXhr(request, xhr); 16 | xhr.status = 200; 17 | xhr.response = 'fake response'; 18 | xhr.onload(); 19 | return send.then(() => { 20 | expect(xhr.upload).not.toHaveProperty('onprogress'); 21 | }); 22 | }); 23 | 24 | test('upload progress event is not assigned if the request includes a body', () => { 25 | const request = { id: 'fake', body: 'really fake' }; 26 | const xhr = { 27 | send: jest.fn(), 28 | upload: {}, 29 | getAllResponseHeaders: jest.fn() 30 | }; 31 | 32 | const send = browserLayer.sendRequestXhr(request, xhr); 33 | xhr.status = 200; 34 | xhr.response = 'fake response'; 35 | xhr.onload(); 36 | return send.then(() => { 37 | expect(xhr.upload).not.toHaveProperty('onprogress'); 38 | }); 39 | }); 40 | 41 | test('upload progress event is assigned if the request includes a file', () => { 42 | const request = { id: 'fake', file: {} }; 43 | const xhr = { 44 | send: jest.fn(), 45 | upload: {}, 46 | getAllResponseHeaders: jest.fn() 47 | }; 48 | 49 | const send = browserLayer.sendRequestXhr(request, xhr); 50 | xhr.status = 200; 51 | xhr.response = 'fake response'; 52 | xhr.onload(); 53 | return send.then(() => { 54 | expect(xhr.upload).toHaveProperty('onprogress'); 55 | }); 56 | }); 57 | 58 | test('upload progress event is normalized and emitted', () => { 59 | const request = { 60 | id: 'fake', 61 | file: {}, 62 | emitter: { 63 | emit: jest.fn() 64 | } 65 | }; 66 | const xhr = { 67 | send: jest.fn(), 68 | upload: {}, 69 | getAllResponseHeaders: jest.fn() 70 | }; 71 | const mockEvent = { 72 | total: 26, 73 | loaded: 13 74 | }; 75 | 76 | const send = browserLayer.sendRequestXhr(request, xhr); 77 | xhr.status = 200; 78 | xhr.response = 'fake response'; 79 | xhr.onload(); 80 | return send.then(() => { 81 | xhr.upload.onprogress(mockEvent); 82 | expect(request.emitter.emit).toHaveBeenCalledWith( 83 | constants.EVENT_PROGRESS_UPLOAD, 84 | { 85 | total: 26, 86 | transferred: 13, 87 | percent: 50 88 | } 89 | ); 90 | }); 91 | }); 92 | 93 | test('XHR-initialization error causes Promise to reject', () => { 94 | const request = { id: 'fake', body: 'really fake' }; 95 | const xhr = { 96 | send: jest.fn(), 97 | upload: {}, 98 | getAllResponseHeaders: jest.fn() 99 | }; 100 | const mockError = new Error(); 101 | 102 | const send = browserLayer.sendRequestXhr(request, xhr); 103 | xhr.status = 200; 104 | xhr.response = 'fake response'; 105 | xhr.onerror(mockError); 106 | expect(send).rejects.toThrow(mockError); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /lib/browser/browser-client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var browser = require('./browser-layer'); 4 | var MapiClient = require('../classes/mapi-client'); 5 | 6 | function BrowserClient(options) { 7 | MapiClient.call(this, options); 8 | } 9 | BrowserClient.prototype = Object.create(MapiClient.prototype); 10 | BrowserClient.prototype.constructor = BrowserClient; 11 | 12 | BrowserClient.prototype.sendRequest = browser.browserSend; 13 | BrowserClient.prototype.abortRequest = browser.browserAbort; 14 | 15 | /** 16 | * Create a client for the browser. 17 | * 18 | * @param {Object} options 19 | * @param {string} options.accessToken 20 | * @param {string} [options.origin] 21 | * @returns {MapiClient} 22 | */ 23 | function createBrowserClient(options) { 24 | return new BrowserClient(options); 25 | } 26 | 27 | module.exports = createBrowserClient; 28 | -------------------------------------------------------------------------------- /lib/browser/browser-layer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var MapiResponse = require('../classes/mapi-response'); 4 | var MapiError = require('../classes/mapi-error'); 5 | var constants = require('../constants'); 6 | var parseHeaders = require('../helpers/parse-headers'); 7 | 8 | // Keys are request IDs, values are XHRs. 9 | var requestsUnderway = {}; 10 | 11 | function browserAbort(request) { 12 | var xhr = requestsUnderway[request.id]; 13 | if (!xhr) return; 14 | xhr.abort(); 15 | delete requestsUnderway[request.id]; 16 | } 17 | 18 | function createResponse(request, xhr) { 19 | return new MapiResponse(request, { 20 | body: xhr.response, 21 | headers: parseHeaders(xhr.getAllResponseHeaders()), 22 | statusCode: xhr.status 23 | }); 24 | } 25 | 26 | function normalizeBrowserProgressEvent(event) { 27 | var total = event.total; 28 | var transferred = event.loaded; 29 | var percent = (100 * transferred) / total; 30 | return { 31 | total: total, 32 | transferred: transferred, 33 | percent: percent 34 | }; 35 | } 36 | 37 | function sendRequestXhr(request, xhr) { 38 | return new Promise(function(resolve, reject) { 39 | xhr.onprogress = function(event) { 40 | request.emitter.emit( 41 | constants.EVENT_PROGRESS_DOWNLOAD, 42 | normalizeBrowserProgressEvent(event) 43 | ); 44 | }; 45 | 46 | var file = request.file; 47 | if (file) { 48 | xhr.upload.onprogress = function(event) { 49 | request.emitter.emit( 50 | constants.EVENT_PROGRESS_UPLOAD, 51 | normalizeBrowserProgressEvent(event) 52 | ); 53 | }; 54 | } 55 | 56 | xhr.onerror = function(error) { 57 | reject(error); 58 | }; 59 | 60 | xhr.onabort = function() { 61 | var mapiError = new MapiError({ 62 | request: request, 63 | type: constants.ERROR_REQUEST_ABORTED 64 | }); 65 | reject(mapiError); 66 | }; 67 | 68 | xhr.onload = function() { 69 | delete requestsUnderway[request.id]; 70 | if (xhr.status < 200 || xhr.status >= 400) { 71 | var mapiError = new MapiError({ 72 | request: request, 73 | body: xhr.response, 74 | statusCode: xhr.status 75 | }); 76 | reject(mapiError); 77 | return; 78 | } 79 | resolve(xhr); 80 | }; 81 | 82 | var body = request.body; 83 | 84 | // matching service needs to send a www-form-urlencoded request 85 | if (typeof body === 'string') { 86 | xhr.send(body); 87 | } else if (body) { 88 | xhr.send(JSON.stringify(body)); 89 | } else if (file) { 90 | xhr.send(file); 91 | } else { 92 | xhr.send(); 93 | } 94 | 95 | requestsUnderway[request.id] = xhr; 96 | }).then(function(xhr) { 97 | return createResponse(request, xhr); 98 | }); 99 | } 100 | 101 | // The accessToken argument gives this function flexibility 102 | // for Mapbox's internal client. 103 | function createRequestXhr(request, accessToken) { 104 | var url = request.url(accessToken); 105 | var xhr = new window.XMLHttpRequest(); 106 | xhr.open(request.method, url); 107 | Object.keys(request.headers).forEach(function(key) { 108 | xhr.setRequestHeader(key, request.headers[key]); 109 | }); 110 | return xhr; 111 | } 112 | 113 | function browserSend(request) { 114 | return Promise.resolve().then(function() { 115 | var xhr = createRequestXhr(request, request.client.accessToken); 116 | return sendRequestXhr(request, xhr); 117 | }); 118 | } 119 | 120 | module.exports = { 121 | browserAbort: browserAbort, 122 | sendRequestXhr: sendRequestXhr, 123 | browserSend: browserSend, 124 | createRequestXhr: createRequestXhr 125 | }; 126 | -------------------------------------------------------------------------------- /lib/classes/__tests__/mapi-client.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MapiClient = require('../mapi-client'); 4 | const tu = require('../../../test/test-utils'); 5 | const constants = require('../../constants'); 6 | const mbxStatic = require('../../../services/static'); 7 | 8 | test('errors without options', () => { 9 | tu.expectError( 10 | () => { 11 | new MapiClient(); 12 | }, 13 | error => { 14 | expect(error.message).toMatch(/access token/); 15 | } 16 | ); 17 | }); 18 | 19 | test('errors without an accessToken option', () => { 20 | tu.expectError( 21 | () => { 22 | new MapiClient({ foo: 'bar' }); 23 | }, 24 | error => { 25 | expect(error.message).toMatch(/access token/); 26 | } 27 | ); 28 | }); 29 | 30 | test('errors with an invalid accessToken', () => { 31 | tu.expectError( 32 | () => { 33 | new MapiClient({ accessToken: 'bar' }); 34 | }, 35 | error => { 36 | expect(error.message).toMatch(/invalid token/i); 37 | } 38 | ); 39 | }); 40 | 41 | test('origin defaults to the standard public origin', () => { 42 | const client = new MapiClient({ accessToken: tu.mockToken() }); 43 | expect(client.origin).toBe(constants.API_ORIGIN); 44 | }); 45 | 46 | test('properly adds access token to url() when a service client has token', () => { 47 | const client = new MapiClient({ 48 | accessToken: tu.mockToken() 49 | }); 50 | const staticServ = mbxStatic(client); 51 | expect(staticServ.client.accessToken).toBe(client.accessToken); 52 | 53 | expect( 54 | staticServ 55 | .getStaticImage({ 56 | ownerId: 'mapbox', 57 | styleId: 'streets-v11', 58 | width: 200, 59 | height: 300, 60 | position: { 61 | coordinates: [12, 13], 62 | zoom: 4 63 | } 64 | }) 65 | .url() 66 | ).toMatch(client.accessToken); 67 | }); 68 | 69 | test('properly adds access token to url() when service token is passed in', () => { 70 | const staticServ = mbxStatic({ 71 | accessToken: tu.mockToken() 72 | }); 73 | 74 | expect( 75 | staticServ 76 | .getStaticImage({ 77 | ownerId: 'mapbox', 78 | styleId: 'streets-v11', 79 | width: 200, 80 | height: 300, 81 | position: { 82 | coordinates: [12, 13], 83 | zoom: 4 84 | } 85 | }) 86 | .url() 87 | ).toMatch(staticServ.client.accessToken); 88 | }); 89 | -------------------------------------------------------------------------------- /lib/classes/__tests__/mapi-error.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MapiError = require('../mapi-error'); 4 | const constants = require('../../constants'); 5 | 6 | const mockRequest = {}; 7 | 8 | test('HTTP error with body that includes a message, and no options.message', () => { 9 | const error = new MapiError({ 10 | request: mockRequest, 11 | statusCode: 401, 12 | body: '{"message":"You cannot see this"}' 13 | }); 14 | expect(error.type).toBe(constants.ERROR_HTTP); 15 | expect(error.statusCode).toBe(401); 16 | expect(error.body).toEqual({ message: 'You cannot see this' }); 17 | expect(error.message).toBe('You cannot see this'); 18 | expect(error.request).toBe(mockRequest); 19 | }); 20 | 21 | test('HTTP error with body that includes a message, but options.message overrides', () => { 22 | const error = new MapiError({ 23 | request: mockRequest, 24 | statusCode: 401, 25 | body: '{"message":"You cannot see this"}', 26 | message: 'This is important' 27 | }); 28 | expect(error.type).toBe(constants.ERROR_HTTP); 29 | expect(error.statusCode).toBe(401); 30 | expect(error.body).toEqual({ message: 'You cannot see this' }); 31 | expect(error.message).toBe('This is important'); 32 | expect(error.request).toBe(mockRequest); 33 | }); 34 | 35 | test('HTTP error with body that does not include a message, and no options.message', () => { 36 | const error = new MapiError({ 37 | request: mockRequest, 38 | statusCode: 477, 39 | body: '{"foo":"bar"}' 40 | }); 41 | expect(error.type).toBe(constants.ERROR_HTTP); 42 | expect(error.statusCode).toBe(477); 43 | expect(error.body).toEqual({ foo: 'bar' }); 44 | expect(error.message).toBeNull(); 45 | expect(error.request).toBe(mockRequest); 46 | }); 47 | 48 | test('HTTP error with body that is a string', () => { 49 | const error = new MapiError({ 50 | request: mockRequest, 51 | statusCode: 477, 52 | body: 'Hello' 53 | }); 54 | expect(error.type).toBe(constants.ERROR_HTTP); 55 | expect(error.statusCode).toBe(477); 56 | expect(error.body).toBe('Hello'); 57 | expect(error.message).toBe('Hello'); 58 | expect(error.request).toBe(mockRequest); 59 | }); 60 | 61 | test('HTTP error with body that cannot be parsed as JSON', () => { 62 | const error = new MapiError({ 63 | request: mockRequest, 64 | statusCode: 477, 65 | body: '{Hello}' 66 | }); 67 | expect(error.type).toBe(constants.ERROR_HTTP); 68 | expect(error.statusCode).toBe(477); 69 | expect(error.body).toBe('{Hello}'); 70 | expect(error.message).toBe('{Hello}'); 71 | expect(error.request).toBe(mockRequest); 72 | }); 73 | 74 | test('HTTP error with body that does not include a message, but options.message is provided', () => { 75 | const error = new MapiError({ 76 | request: mockRequest, 77 | statusCode: 477, 78 | body: '{"foo":"bar"}', 79 | message: 'This is important' 80 | }); 81 | expect(error.type).toBe(constants.ERROR_HTTP); 82 | expect(error.statusCode).toBe(477); 83 | expect(error.body).toEqual({ foo: 'bar' }); 84 | expect(error.message).toBe('This is important'); 85 | expect(error.request).toBe(mockRequest); 86 | }); 87 | 88 | test('HTTP error with no body or options.message', () => { 89 | const error = new MapiError({ 90 | request: mockRequest, 91 | statusCode: 500 92 | }); 93 | expect(error.type).toBe(constants.ERROR_HTTP); 94 | expect(error.statusCode).toBe(500); 95 | expect(error.body).toBeNull(); 96 | expect(error.message).toBeNull(); 97 | expect(error.request).toBe(mockRequest); 98 | }); 99 | 100 | test('HTTP error with options.message but no body', () => { 101 | const error = new MapiError({ 102 | request: mockRequest, 103 | statusCode: 500, 104 | message: 'Oops' 105 | }); 106 | expect(error.type).toBe(constants.ERROR_HTTP); 107 | expect(error.statusCode).toBe(500); 108 | expect(error.body).toBeNull(); 109 | expect(error.message).toBe('Oops'); 110 | expect(error.request).toBe(mockRequest); 111 | }); 112 | 113 | test('RequestAbortedError', () => { 114 | const error = new MapiError({ 115 | request: mockRequest, 116 | type: constants.ERROR_REQUEST_ABORTED 117 | }); 118 | expect(error.type).toBe(constants.ERROR_REQUEST_ABORTED); 119 | expect(error.statusCode).toBeNull(); 120 | expect(error.body).toBeNull(); 121 | expect(error.message).toBe('Request aborted'); 122 | expect(error.request).toBe(mockRequest); 123 | }); 124 | -------------------------------------------------------------------------------- /lib/classes/__tests__/mapi-response.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | jest.mock('../../helpers/parse-link-header', () => { 4 | return jest.fn(() => ({ parsed: 'link-header' })); 5 | }); 6 | 7 | const MapiResponse = require('../mapi-response'); 8 | const MapiRequest = require('../mapi-request'); 9 | const parseLinkHeader = require('../../helpers/parse-link-header'); 10 | 11 | function dummyRequest() { 12 | return new MapiRequest( 13 | {}, 14 | { 15 | method: 'GET', 16 | path: '/styles/v1/mockuser' 17 | } 18 | ); 19 | } 20 | 21 | describe('MapiResponse', () => { 22 | test('sets public instance fields', () => { 23 | const request = dummyRequest(); 24 | const response = new MapiResponse(request, { 25 | headers: { 26 | mock: true, 27 | link: 'yaya' 28 | }, 29 | body: '{ "mock": "body" }' 30 | }); 31 | expect(response).toHaveProperty('request', request); 32 | expect(response).toHaveProperty('rawBody', '{ "mock": "body" }'); 33 | expect(response).toHaveProperty('body', { 34 | mock: 'body' 35 | }); 36 | expect(response).toHaveProperty('headers', { mock: true, link: 'yaya' }); 37 | expect(response).toHaveProperty('links', { 38 | parsed: 'link-header' 39 | }); 40 | expect(parseLinkHeader).toHaveBeenCalledWith('yaya'); 41 | }); 42 | }); 43 | 44 | describe('MapiResponse#hasNextPage', () => { 45 | test('returns true if parsed links include a next page', () => { 46 | const request = dummyRequest(); 47 | parseLinkHeader.mockReturnValueOnce({ next: { url: 'https://weep.com' } }); 48 | const response = new MapiResponse(request, { 49 | headers: { mock: true }, 50 | body: '{ "mock": "body" }' 51 | }); 52 | expect(response.hasNextPage()).toBe(true); 53 | }); 54 | 55 | test('returns false if parsed link do not include a next page', () => { 56 | const request = dummyRequest(); 57 | parseLinkHeader.mockReturnValueOnce({ blah: { url: 'https://weep.com' } }); 58 | const response = new MapiResponse(request, { 59 | headers: { mock: true }, 60 | body: '{ "mock": "body" }' 61 | }); 62 | expect(response.hasNextPage()).toBe(false); 63 | }); 64 | }); 65 | 66 | describe('MapiResopnse#nextPage', () => { 67 | test('returns null if there is no next page', () => { 68 | const request = dummyRequest(); 69 | parseLinkHeader.mockReturnValueOnce({ blah: 'https://weep.com' }); 70 | const response = new MapiResponse(request, { 71 | headers: { mock: true }, 72 | body: '{ "mock": "body" }' 73 | }); 74 | expect(response.nextPage()).toBeNull(); 75 | }); 76 | 77 | test('returns a new request if there is a next page', () => { 78 | const request = new MapiRequest( 79 | {}, 80 | { 81 | method: 'PATCH', 82 | path: '/styles/v1/mockuser' 83 | } 84 | ); 85 | parseLinkHeader.mockReturnValueOnce({ next: { url: 'https://weep.com' } }); 86 | const response = new MapiResponse(request, { 87 | headers: { mock: true }, 88 | body: '{ "mock": "body" }' 89 | }); 90 | const nextPageRequest = response.nextPage(); 91 | expect(nextPageRequest).toBeInstanceOf(MapiRequest); 92 | expect(nextPageRequest).toMatchObject({ 93 | path: 'https://weep.com', 94 | method: 'PATCH' 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /lib/classes/mapi-client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var parseToken = require('@mapbox/parse-mapbox-token'); 4 | var MapiRequest = require('./mapi-request'); 5 | var constants = require('../constants'); 6 | 7 | /** 8 | * A low-level Mapbox API client. Use it to create service clients 9 | * that share the same configuration. 10 | * 11 | * Services and `MapiRequest`s use the underlying `MapiClient` to 12 | * determine how to create, send, and abort requests in a way 13 | * that is appropriate to the configuration and environment 14 | * (Node or the browser). 15 | * 16 | * @class MapiClient 17 | * @property {string} accessToken - The Mapbox access token assigned 18 | * to this client. 19 | * @property {string} [origin] - The origin 20 | * to use for API requests. Defaults to https://api.mapbox.com. 21 | */ 22 | 23 | function MapiClient(options) { 24 | if (!options || !options.accessToken) { 25 | throw new Error('Cannot create a client without an access token'); 26 | } 27 | // Try parsing the access token to determine right away if it's valid. 28 | parseToken(options.accessToken); 29 | 30 | this.accessToken = options.accessToken; 31 | this.origin = options.origin || constants.API_ORIGIN; 32 | } 33 | 34 | MapiClient.prototype.createRequest = function createRequest(requestOptions) { 35 | return new MapiRequest(this, requestOptions); 36 | }; 37 | 38 | module.exports = MapiClient; 39 | -------------------------------------------------------------------------------- /lib/classes/mapi-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var constants = require('../constants'); 4 | 5 | /** 6 | * A Mapbox API error. 7 | * 8 | * If there's an error during the API transaction, 9 | * the Promise returned by `MapiRequest`'s [`send`](#send) 10 | * method should reject with a `MapiError`. 11 | * 12 | * @class MapiError 13 | * @hideconstructor 14 | * @property {MapiRequest} request - The errored request. 15 | * @property {string} type - The type of error. Usually this is `'HttpError'`. 16 | * If the request was aborted, so the error was 17 | * not sent from the server, the type will be 18 | * `'RequestAbortedError'`. 19 | * @property {number} [statusCode] - The numeric status code of 20 | * the HTTP response. 21 | * @property {Object | string} [body] - If the server sent a response body, 22 | * this property exposes that response, parsed as JSON if possible. 23 | * @property {string} [message] - Whatever message could be derived from the 24 | * call site and HTTP response. 25 | * 26 | * @param {MapiRequest} options.request 27 | * @param {number} [options.statusCode] 28 | * @param {string} [options.body] 29 | * @param {string} [options.message] 30 | * @param {string} [options.type] 31 | */ 32 | function MapiError(options) { 33 | var errorType = options.type || constants.ERROR_HTTP; 34 | 35 | var body; 36 | if (options.body) { 37 | try { 38 | body = JSON.parse(options.body); 39 | } catch (e) { 40 | body = options.body; 41 | } 42 | } else { 43 | body = null; 44 | } 45 | 46 | var message = options.message || null; 47 | if (!message) { 48 | if (typeof body === 'string') { 49 | message = body; 50 | } else if (body && typeof body.message === 'string') { 51 | message = body.message; 52 | } else if (errorType === constants.ERROR_REQUEST_ABORTED) { 53 | message = 'Request aborted'; 54 | } 55 | } 56 | 57 | this.message = message; 58 | this.type = errorType; 59 | this.statusCode = options.statusCode || null; 60 | this.request = options.request; 61 | this.body = body; 62 | } 63 | 64 | module.exports = MapiError; 65 | -------------------------------------------------------------------------------- /lib/classes/mapi-response.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var parseLinkHeader = require('../helpers/parse-link-header'); 4 | 5 | /** 6 | * A Mapbox API response. 7 | * 8 | * @class MapiResponse 9 | * @property {Object} body - The response body, parsed as JSON. 10 | * @property {string} rawBody - The raw response body. 11 | * @property {number} statusCode - The response's status code. 12 | * @property {Object} headers - The parsed response headers. 13 | * @property {Object} links - The parsed response links. 14 | * @property {MapiRequest} request - The response's originating `MapiRequest`. 15 | */ 16 | 17 | /** 18 | * @ignore 19 | * @param {MapiRequest} request 20 | * @param {Object} responseData 21 | * @param {Object} responseData.headers 22 | * @param {string} responseData.body 23 | * @param {number} responseData.statusCode 24 | */ 25 | function MapiResponse(request, responseData) { 26 | this.request = request; 27 | this.headers = responseData.headers; 28 | this.rawBody = responseData.body; 29 | this.statusCode = responseData.statusCode; 30 | try { 31 | this.body = JSON.parse(responseData.body || '{}'); 32 | } catch (parseError) { 33 | this.body = responseData.body; 34 | } 35 | this.links = parseLinkHeader(this.headers.link); 36 | } 37 | 38 | /** 39 | * Check if there is a next page that you can fetch. 40 | * 41 | * @returns {boolean} 42 | */ 43 | MapiResponse.prototype.hasNextPage = function hasNextPage() { 44 | return !!this.links.next; 45 | }; 46 | 47 | /** 48 | * Create a request for the next page, if there is one. 49 | * If there is no next page, returns `null`. 50 | * 51 | * @returns {MapiRequest | null} 52 | */ 53 | MapiResponse.prototype.nextPage = function nextPage() { 54 | if (!this.hasNextPage()) return null; 55 | return this.request._extend({ 56 | path: this.links.next.url 57 | }); 58 | }; 59 | 60 | module.exports = MapiResponse; 61 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // The "browser" field in "package.json" instructs browser 4 | // bundlers to override this an load browser/browser-client, instead. 5 | var nodeClient = require('./node/node-client'); 6 | 7 | module.exports = nodeClient; 8 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | API_ORIGIN: 'https://api.mapbox.com', 5 | EVENT_PROGRESS_DOWNLOAD: 'downloadProgress', 6 | EVENT_PROGRESS_UPLOAD: 'uploadProgress', 7 | EVENT_ERROR: 'error', 8 | EVENT_RESPONSE: 'response', 9 | ERROR_HTTP: 'HttpError', 10 | ERROR_REQUEST_ABORTED: 'RequestAbortedError' 11 | }; 12 | -------------------------------------------------------------------------------- /lib/helpers/__tests__/parse-headers.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const parseHeaders = require('../parse-headers'); 4 | 5 | test('works', () => { 6 | const raw = `date: Fri, 08 Dec 2017 21:04:30 GMT 7 | content-encoding: gzip 8 | x-content-type-options: nosniff 9 | server: meinheld/0.6.1 10 | x-frame-options: DENY 11 | content-type: text/html; charset=utf-8 12 | connection: keep-alive 13 | 14 | 15 | strict-transport-security: max-age=63072000 16 | vary: Cookie, Accept-Encoding 17 | content-length: 6502 18 | x-xss-protection: 1; mode=block`; 19 | 20 | expect(parseHeaders(raw)).toEqual({ 21 | connection: 'keep-alive', 22 | 'content-encoding': 'gzip', 23 | 'content-length': '6502', 24 | 'content-type': 'text/html; charset=utf-8', 25 | date: 'Fri, 08 Dec 2017 21:04:30 GMT', 26 | server: 'meinheld/0.6.1', 27 | 'strict-transport-security': 'max-age=63072000', 28 | vary: 'Cookie, Accept-Encoding', 29 | 'x-content-type-options': 'nosniff', 30 | 'x-frame-options': 'DENY', 31 | 'x-xss-protection': '1; mode=block' 32 | }); 33 | }); 34 | 35 | test('given empty input, returns empty object', () => { 36 | expect(parseHeaders()).toEqual({}); 37 | expect(parseHeaders(undefined)).toEqual({}); 38 | expect(parseHeaders(null)).toEqual({}); 39 | expect(parseHeaders('')).toEqual({}); 40 | }); 41 | -------------------------------------------------------------------------------- /lib/helpers/__tests__/parse-link-header.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Tests mirror tests in https://github.com/thlorenz/parse-link-header 4 | const parseLinkHeader = require('../parse-link-header'); 5 | 6 | test('parsing a proper link header with next and last', () => { 7 | const link = 8 | '; rel="next", ' + 9 | '; rel="last"'; 10 | 11 | expect(parseLinkHeader(link)).toEqual({ 12 | next: { 13 | url: 14 | 'https://api.github.com/user/9287/repos?client_id=1&client_secret=2&page=2&per_page=100', 15 | params: {} 16 | }, 17 | last: { 18 | url: 19 | 'https://api.github.com/user/9287/repos?client_id=1&client_secret=2&page=3&per_page=100', 20 | params: {} 21 | } 22 | }); 23 | }); 24 | 25 | test('handles unquoted relationships', () => { 26 | const link = 27 | '; rel=next, ' + 28 | '; rel=last'; 29 | 30 | expect(parseLinkHeader(link)).toEqual({ 31 | next: { 32 | params: {}, 33 | url: 34 | 'https://api.github.com/user/9287/repos?client_id=1&client_secret=2&page=2&per_page=100' 35 | }, 36 | last: { 37 | params: {}, 38 | url: 39 | 'https://api.github.com/user/9287/repos?client_id=1&client_secret=2&page=3&per_page=100' 40 | } 41 | }); 42 | }); 43 | 44 | test('parsing a proper link header with next, prev and last', () => { 45 | const linkHeader = 46 | '; rel="next", ' + 47 | '; rel="prev", ' + 48 | '; rel="last"'; 49 | 50 | expect(parseLinkHeader(linkHeader)).toEqual({ 51 | next: { 52 | params: {}, 53 | url: 'https://api.github.com/user/9287/repos?page=3&per_page=100' 54 | }, 55 | prev: { 56 | params: {}, 57 | url: 'https://api.github.com/user/9287/repos?page=1&per_page=100' 58 | }, 59 | last: { 60 | params: {}, 61 | url: 'https://api.github.com/user/9287/repos?page=5&per_page=100' 62 | } 63 | }); 64 | }); 65 | 66 | test('parsing an empty link header', () => { 67 | const linkHeader = ''; 68 | expect(parseLinkHeader(linkHeader)).toEqual({}); 69 | }); 70 | 71 | test('parsing a proper link header with next and a link without rel', () => { 72 | const linkHeader = 73 | '; rel="next", ' + 74 | '; pet="cat", '; 75 | 76 | expect(parseLinkHeader(linkHeader)).toEqual({ 77 | next: { 78 | params: {}, 79 | url: 'https://api.github.com/user/9287/repos?page=3&per_page=100' 80 | } 81 | }); 82 | }); 83 | 84 | test('parsing a proper link header with next and properties besides rel', () => { 85 | const linkHeader = 86 | '; rel="next"; hello="world"; pet="cat"'; 87 | 88 | expect(parseLinkHeader(linkHeader)).toEqual({ 89 | next: { 90 | params: { 91 | hello: 'world', 92 | pet: 'cat' 93 | }, 94 | url: 'https://api.github.com/user/9287/repos?page=3&per_page=100' 95 | } 96 | }); 97 | }); 98 | 99 | test('parsing a proper link header with a comma in the url', () => { 100 | const linkHeader = 101 | '; rel="next";'; 102 | 103 | expect(parseLinkHeader(linkHeader)).toEqual({ 104 | next: { 105 | params: {}, 106 | url: 'https://imaginary.url.notreal/?name=What,+me+worry' 107 | } 108 | }); 109 | }); 110 | 111 | test('parsing a proper link header with a multi-word rel', () => { 112 | const linkHeader = 113 | '; rel="next page";'; 114 | 115 | expect(parseLinkHeader(linkHeader)).toEqual({ 116 | next: { 117 | params: {}, 118 | url: 'https://imaginary.url.notreal/?name=What,+me+worry' 119 | }, 120 | page: { 121 | params: {}, 122 | url: 'https://imaginary.url.notreal/?name=What,+me+worry' 123 | } 124 | }); 125 | }); 126 | 127 | test('parsing a proper link header with matrix parameters', () => { 128 | const linkHeader = 129 | '; rel="next";'; 130 | 131 | expect(parseLinkHeader(linkHeader)).toEqual({ 132 | next: { 133 | params: {}, 134 | url: 135 | 'https://imaginary.url.notreal/segment;foo=bar;baz/item?name=What,+me+worry' 136 | } 137 | }); 138 | }); 139 | 140 | test('invalid header', () => { 141 | expect(parseLinkHeader('<;')).toEqual({}); 142 | expect(parseLinkHeader('')).toEqual({}); 143 | expect(parseLinkHeader(';,<7')).toEqual({}); 144 | }); 145 | 146 | test('empty header', () => { 147 | expect(parseLinkHeader('<>;;')).toEqual({}); 148 | }); 149 | 150 | // See https://tools.ietf.org/html/rfc5988#section-5.3 151 | test('if multiple rel parameters are provided, the first is used', () => { 152 | const link = 153 | '; rel="next"; rel="previous"'; 154 | 155 | expect(parseLinkHeader(link)).toEqual({ 156 | next: { 157 | url: 158 | 'https://api.github.com/user/9287/repos?client_id=1&client_secret=2&page=2&per_page=100', 159 | params: {} 160 | } 161 | }); 162 | }); 163 | 164 | test('if multiple rel values are provided with a duplicate, the duplicate is ignored', () => { 165 | const link = 166 | '; rel="next next"'; 167 | 168 | expect(parseLinkHeader(link)).toEqual({ 169 | next: { 170 | url: 171 | 'https://api.github.com/user/9287/repos?client_id=1&client_secret=2&page=2&per_page=100', 172 | params: {} 173 | } 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /lib/helpers/__tests__/url-utils.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const urlUtils = require('../url-utils'); 4 | 5 | const { 6 | appendQueryParam, 7 | appendQueryObject, 8 | prependOrigin, 9 | interpolateRouteParams 10 | } = urlUtils; 11 | 12 | describe('appendQueryParam', () => { 13 | test('appends when no search exists in source URL', () => { 14 | expect(appendQueryParam('foo', 'bar', 'baz')).toBe('foo?bar=baz'); 15 | }); 16 | 17 | test('appends when one search param exists in source URL', () => { 18 | expect(appendQueryParam('foo?bar=baz', 'cat', 'cyrus')).toBe( 19 | 'foo?bar=baz&cat=cyrus' 20 | ); 21 | }); 22 | 23 | test('appends when three search params exist in source URL', () => { 24 | expect( 25 | appendQueryParam( 26 | 'foo?bar=baz&cat=cyrus&dog=penny&horse=ed', 27 | 'mouse', 28 | 'peter' 29 | ) 30 | ).toBe('foo?bar=baz&cat=cyrus&dog=penny&horse=ed&mouse=peter'); 31 | }); 32 | 33 | test('encodes everything except commas separating items in array values', () => { 34 | expect(appendQueryParam('foo', '#bar', ['a?', 'b,d', 'c&'])).toBe( 35 | 'foo?%23bar=a%3F,b%2Cd,c%26' 36 | ); 37 | }); 38 | }); 39 | 40 | describe('appendQueryObject', () => { 41 | test('returns same URL if query is empty', () => { 42 | expect(appendQueryObject('/foo/bar')).toBe('/foo/bar'); 43 | }); 44 | 45 | test('attaches new query after existing query', () => { 46 | expect( 47 | appendQueryObject('/foo/bar?one=1', { 48 | two: 2, 49 | three: 3 50 | }) 51 | ).toBe('/foo/bar?one=1&two=2&three=3'); 52 | }); 53 | 54 | test('handles primitives', () => { 55 | expect( 56 | appendQueryObject('/foo/bar', { 57 | one: 1, 58 | two: 2, 59 | three: '3', 60 | four: true, 61 | five: false 62 | }) 63 | ).toBe('/foo/bar?one=1&two=2&three=3&four'); 64 | }); 65 | 66 | test('turns arrays into comma lists', () => { 67 | expect( 68 | appendQueryObject('/foo/bar', { 69 | test: ['one', 'two'] 70 | }) 71 | ).toBe('/foo/bar?test=one%2Ctwo'); 72 | }); 73 | 74 | test('handles arrays with zero values correctly', () => { 75 | expect( 76 | appendQueryObject('/geocoding/v5/mapbox.places/berlin.json', { 77 | proximity: [0, 51.5] 78 | }) 79 | ).toBe('/geocoding/v5/mapbox.places/berlin.json?proximity=0%2C51.5'); 80 | }); 81 | 82 | test('skips undefined properties', () => { 83 | expect( 84 | appendQueryObject('/foo/bar', { 85 | one: undefined, 86 | two: 2 87 | }) 88 | ).toBe('/foo/bar?two=2'); 89 | }); 90 | }); 91 | 92 | describe('prependOrigin', () => { 93 | test('returns original if no origin is provided', () => { 94 | expect(prependOrigin('foo/bar')).toBe('foo/bar'); 95 | }); 96 | 97 | test('does not replace origin if one was already there', () => { 98 | expect( 99 | prependOrigin('http://www.first.com/foo/bar', 'https://second.com') 100 | ).toBe('http://www.first.com/foo/bar'); 101 | }); 102 | 103 | test('always ends up with just one slash between the origin and the path', () => { 104 | expect(prependOrigin('foo/bar', 'https://test.com/')).toBe( 105 | 'https://test.com/foo/bar' 106 | ); 107 | expect(prependOrigin('/foo/bar', 'https://test.com/')).toBe( 108 | 'https://test.com/foo/bar' 109 | ); 110 | expect(prependOrigin('/foo/bar', 'https://test.com')).toBe( 111 | 'https://test.com/foo/bar' 112 | ); 113 | expect(prependOrigin('foo/bar', 'https://test.com')).toBe( 114 | 'https://test.com/foo/bar' 115 | ); 116 | }); 117 | }); 118 | 119 | describe('interpolateRouteParams', () => { 120 | test('returns route if no params are provided', () => { 121 | expect(interpolateRouteParams('/foo/bar')).toBe('/foo/bar'); 122 | }); 123 | 124 | test('encodes all values except commas separating items in array values', () => { 125 | expect( 126 | interpolateRouteParams('/foo/:a/:b/:c', { 127 | a: '#bar', 128 | b: ['a?', 'c&'], 129 | c: 'b,d' 130 | }) 131 | ).toBe('/foo/%23bar/a%3F,c%26/b%2Cd'); 132 | }); 133 | 134 | test('can interpolate filenames before extensions', () => { 135 | expect( 136 | interpolateRouteParams('/foo/:a.json', { 137 | a: ['1', '2'] 138 | }) 139 | ).toBe('/foo/1,2.json'); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /lib/helpers/parse-headers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function parseSingleHeader(raw) { 4 | var boundary = raw.indexOf(':'); 5 | var name = raw 6 | .substring(0, boundary) 7 | .trim() 8 | .toLowerCase(); 9 | var value = raw.substring(boundary + 1).trim(); 10 | return { 11 | name: name, 12 | value: value 13 | }; 14 | } 15 | 16 | /** 17 | * Parse raw headers into an object with lowercase properties. 18 | * Does not fully parse headings into more complete data structure, 19 | * as larger libraries might do. Also does not deal with duplicate 20 | * headers because Node doesn't seem to deal with those well, so 21 | * we shouldn't let the browser either, for consistency. 22 | * 23 | * @param {string} raw 24 | * @returns {Object} 25 | */ 26 | function parseHeaders(raw) { 27 | var headers = {}; 28 | if (!raw) { 29 | return headers; 30 | } 31 | 32 | raw 33 | .trim() 34 | .split(/[\r|\n]+/) 35 | .forEach(function(rawHeader) { 36 | var parsed = parseSingleHeader(rawHeader); 37 | headers[parsed.name] = parsed.value; 38 | }); 39 | 40 | return headers; 41 | } 42 | 43 | module.exports = parseHeaders; 44 | -------------------------------------------------------------------------------- /lib/helpers/parse-link-header.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Like https://github.com/thlorenz/lib/parse-link-header but without any 4 | // additional dependencies. 5 | 6 | function parseParam(param) { 7 | var parts = param.match(/\s*(.+)\s*=\s*"?([^"]+)"?/); 8 | if (!parts) return null; 9 | 10 | return { 11 | key: parts[1], 12 | value: parts[2] 13 | }; 14 | } 15 | 16 | function parseLink(link) { 17 | var parts = link.match(/]*)>(.*)/); 18 | if (!parts) return null; 19 | 20 | var linkUrl = parts[1]; 21 | var linkParams = parts[2].split(';'); 22 | var rel = null; 23 | var parsedLinkParams = linkParams.reduce(function(result, param) { 24 | var parsed = parseParam(param); 25 | if (!parsed) return result; 26 | if (parsed.key === 'rel') { 27 | if (!rel) { 28 | rel = parsed.value; 29 | } 30 | return result; 31 | } 32 | result[parsed.key] = parsed.value; 33 | return result; 34 | }, {}); 35 | if (!rel) return null; 36 | 37 | return { 38 | url: linkUrl, 39 | rel: rel, 40 | params: parsedLinkParams 41 | }; 42 | } 43 | 44 | /** 45 | * Parse a Link header. 46 | * 47 | * @param {string} linkHeader 48 | * @returns {{ 49 | * [string]: { 50 | * url: string, 51 | * params: { [string]: string } 52 | * } 53 | * }} 54 | */ 55 | function parseLinkHeader(linkHeader) { 56 | if (!linkHeader) return {}; 57 | 58 | return linkHeader.split(/,\s*>} [value] - Provide an array 22 | * if the value is a list and commas between values need to be 23 | * preserved, unencoded. 24 | * @returns {string} - Modified URL. 25 | */ 26 | function appendQueryParam(url, key, value) { 27 | if (value === false || value === null) { 28 | return url; 29 | } 30 | var punctuation = /\?/.test(url) ? '&' : '?'; 31 | var query = encodeURIComponent(key); 32 | if (value !== undefined && value !== '' && value !== true) { 33 | query += '=' + encodeValue(value); 34 | } 35 | return '' + url + punctuation + query; 36 | } 37 | 38 | /** 39 | * Derive a query string from an object and append it 40 | * to a URL. 41 | * 42 | * @param {string} url 43 | * @param {Object} [queryObject] - Values should be primitives. 44 | * @returns {string} - Modified URL. 45 | */ 46 | function appendQueryObject(url, queryObject) { 47 | if (!queryObject) { 48 | return url; 49 | } 50 | 51 | var result = url; 52 | Object.keys(queryObject).forEach(function(key) { 53 | var value = queryObject[key]; 54 | if (value === undefined) { 55 | return; 56 | } 57 | if (Array.isArray(value)) { 58 | value = value 59 | .filter(function(v) { 60 | return v !== null && v !== undefined; 61 | }) 62 | .join(','); 63 | } 64 | result = appendQueryParam(result, key, value); 65 | }); 66 | return result; 67 | } 68 | 69 | /** 70 | * Prepend an origin to a URL. If the URL already has an 71 | * origin, do nothing. 72 | * 73 | * @param {string} url 74 | * @param {string} origin 75 | * @returns {string} - Modified URL. 76 | */ 77 | function prependOrigin(url, origin) { 78 | if (!origin) { 79 | return url; 80 | } 81 | 82 | if (url.slice(0, 4) === 'http') { 83 | return url; 84 | } 85 | 86 | var delimiter = url[0] === '/' ? '' : '/'; 87 | return '' + origin.replace(/\/$/, '') + delimiter + url; 88 | } 89 | 90 | /** 91 | * Interpolate values into a route with express-style, 92 | * colon-prefixed route parameters. 93 | * 94 | * @param {string} route 95 | * @param {Object} [params] - Values should be primitives 96 | * or arrays of primitives. Provide an array if the value 97 | * is a list and commas between values need to be 98 | * preserved, unencoded. 99 | * @returns {string} - Modified URL. 100 | */ 101 | function interpolateRouteParams(route, params) { 102 | if (!params) { 103 | return route; 104 | } 105 | return route.replace(/\/:([a-zA-Z0-9]+)/g, function(_, paramId) { 106 | var value = params[paramId]; 107 | if (value === undefined) { 108 | throw new Error('Unspecified route parameter ' + paramId); 109 | } 110 | var preppedValue = encodeValue(value); 111 | return '/' + preppedValue; 112 | }); 113 | } 114 | 115 | module.exports = { 116 | appendQueryObject: appendQueryObject, 117 | appendQueryParam: appendQueryParam, 118 | prependOrigin: prependOrigin, 119 | interpolateRouteParams: interpolateRouteParams 120 | }; 121 | -------------------------------------------------------------------------------- /lib/node/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "browser": false, 5 | "es6": true 6 | } 7 | } -------------------------------------------------------------------------------- /lib/node/node-client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var node = require('./node-layer'); 4 | var MapiClient = require('../classes/mapi-client'); 5 | 6 | function NodeClient(options) { 7 | MapiClient.call(this, options); 8 | } 9 | NodeClient.prototype = Object.create(MapiClient.prototype); 10 | NodeClient.prototype.constructor = NodeClient; 11 | 12 | NodeClient.prototype.sendRequest = node.nodeSend; 13 | NodeClient.prototype.abortRequest = node.nodeAbort; 14 | 15 | /** 16 | * Create a client for Node. 17 | * 18 | * @param {Object} options 19 | * @param {string} options.accessToken 20 | * @param {string} [options.origin] 21 | * @returns {MapiClient} 22 | */ 23 | function createNodeClient(options) { 24 | return new NodeClient(options); 25 | } 26 | 27 | module.exports = createNodeClient; 28 | -------------------------------------------------------------------------------- /lib/node/node-layer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var xtend = require('xtend'); 4 | var fs = require('fs'); 5 | var got = require('got'); 6 | var FormData = require('form-data'); 7 | var MapiResponse = require('../classes/mapi-response'); 8 | var MapiError = require('../classes/mapi-error'); 9 | var constants = require('../constants'); 10 | 11 | var methodsWithBodies = new Set(['PUT', 'PATCH', 'POST']); 12 | 13 | // Keys are request IDs, values are objects with 14 | // clientRequest and gotStream properties. 15 | var requestsUnderway = {}; 16 | 17 | function nodeAbort(request) { 18 | var streams = requestsUnderway[request.id]; 19 | if (!streams) return; 20 | streams.clientRequest.abort(); 21 | delete requestsUnderway[request.id]; 22 | } 23 | 24 | function normalizeGotProgressEvent(progress) { 25 | return xtend(progress, { 26 | percent: progress.percent * 100 27 | }); 28 | } 29 | 30 | function createRequestStreams(request) { 31 | var url = request.url(request.client.accessToken); 32 | var gotOptions = { 33 | method: request.method, 34 | headers: request.headers, 35 | retries: 0, 36 | followRedirect: false, 37 | throwHttpErrors: false 38 | }; 39 | 40 | if (typeof request.file === 'string') { 41 | if (request.sendFileAs && request.sendFileAs === 'form') { 42 | const form = new FormData(); 43 | form.append('file', fs.createReadStream(request.file)); 44 | gotOptions.body = form; 45 | } else { 46 | gotOptions.body = fs.createReadStream(request.file); 47 | } 48 | } else if (request.file && request.file.pipe) { 49 | if (request.sendFileAs && request.sendFileAs === 'form') { 50 | const form = new FormData(); 51 | form.append('file', request.file); 52 | gotOptions.body = form; 53 | } else { 54 | gotOptions.body = request.file; 55 | } 56 | } else if (typeof request.body === 'string') { 57 | // matching service needs to send a www-form-urlencoded request 58 | gotOptions.body = request.body; 59 | } else if (request.body) { 60 | gotOptions.body = JSON.stringify(request.body); 61 | } 62 | 63 | if ( 64 | ['POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method) && 65 | !request.body 66 | ) { 67 | gotOptions.body = ''; 68 | } 69 | 70 | var gotStream = got.stream(url, gotOptions); 71 | 72 | gotStream.setEncoding(request.encoding); 73 | 74 | gotStream.on('downloadProgress', function(progress) { 75 | request.emitter.emit( 76 | constants.EVENT_PROGRESS_DOWNLOAD, 77 | normalizeGotProgressEvent(progress) 78 | ); 79 | }); 80 | gotStream.on('uploadProgress', function(progress) { 81 | request.emitter.emit( 82 | constants.EVENT_PROGRESS_UPLOAD, 83 | normalizeGotProgressEvent(progress) 84 | ); 85 | }); 86 | 87 | return new Promise(function(resolve) { 88 | gotStream.on('request', function(req) { 89 | var clientRequest = req; 90 | var streams = { clientRequest: clientRequest, gotStream: gotStream }; 91 | requestsUnderway[request.id] = streams; 92 | resolve(streams); 93 | }); 94 | 95 | // Got will not end the stream for methods that *can* have 96 | // bodies if you don't provide a body, so we'll do it manually. 97 | if ( 98 | methodsWithBodies.has(request.method) && 99 | gotOptions.body === undefined 100 | ) { 101 | gotStream.end(); 102 | } 103 | }); 104 | } 105 | 106 | function nodeSend(request) { 107 | return Promise.resolve() 108 | .then(function() { 109 | return createRequestStreams(request); 110 | }) 111 | .then(function(result) { 112 | return sendStreams(result.gotStream, result.clientRequest); 113 | }); 114 | 115 | function sendStreams(gotStream, clientRequest) { 116 | return new Promise(function(resolve, reject) { 117 | var errored = false; 118 | clientRequest.on('abort', function() { 119 | var mapiError = new MapiError({ 120 | request: request, 121 | type: constants.ERROR_REQUEST_ABORTED 122 | }); 123 | errored = true; 124 | reject(mapiError); 125 | }); 126 | 127 | var httpsResponse = void 0; 128 | var statusCode = void 0; 129 | gotStream.on('response', function(res) { 130 | httpsResponse = res; 131 | statusCode = res.statusCode; 132 | }); 133 | 134 | var body = ''; 135 | gotStream.on('data', function(chunk) { 136 | body += chunk; 137 | }); 138 | 139 | gotStream.on('end', function() { 140 | if (errored || !httpsResponse) return; 141 | 142 | if (statusCode < 200 || statusCode >= 400) { 143 | var mapiError = new MapiError({ 144 | request: request, 145 | body: body, 146 | statusCode: statusCode 147 | }); 148 | reject(mapiError); 149 | return; 150 | } 151 | 152 | try { 153 | var response = new MapiResponse(request, { 154 | body: body, 155 | headers: httpsResponse.headers, 156 | statusCode: httpsResponse.statusCode 157 | }); 158 | resolve(response); 159 | } catch (responseError) { 160 | reject(responseError); 161 | } 162 | }); 163 | 164 | gotStream.on('error', function(error) { 165 | errored = true; 166 | reject(error); 167 | }); 168 | }); 169 | } 170 | } 171 | 172 | module.exports = { 173 | nodeAbort: nodeAbort, 174 | nodeSend: nodeSend 175 | }; 176 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mapbox/mapbox-sdk", 3 | "version": "0.16.1", 4 | "description": "JS SDK for accessing Mapbox APIs", 5 | "main": "index.js", 6 | "files": [ 7 | "umd", 8 | "lib", 9 | "services", 10 | "test/test-shared-interface.js", 11 | "test/test-utils.js" 12 | ], 13 | "scripts": { 14 | "format": "prettier --write '**/*.js'", 15 | "lint-md": "remark-preset-davidtheclark", 16 | "lint-js": "eslint .", 17 | "lint": "run-p --aggregate-output lint-md lint-js", 18 | "pretest": "npm run lint", 19 | "test": "jest", 20 | "try-browser": "budo test/try-browser/try-browser.js:bundle.js -d test/try-browser -l", 21 | "document-services": "documentation build services/service-helpers/generic-types.js 'services/*.js' --shallow --format md --config conf/service-documentation.yml > docs/services.md", 22 | "document-classes": "documentation build 'lib/classes/*.js' --shallow --format md --config conf/classes-documentation.yml > docs/classes.md", 23 | "document": "run-p --aggregate-output document-services document-classes && npm run lint-md", 24 | "precommit": "npm run document && git add docs && lint-staged", 25 | "bundle": "rollup --config ./rollup.config.js && uglifyjs umd/mapbox-sdk.js > umd/mapbox-sdk.min.js", 26 | "prepublishOnly": "npm run bundle" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/mapbox/mapbox-sdk-js.git" 31 | }, 32 | "keywords": [ 33 | "mapbox", 34 | "sdk", 35 | "api", 36 | "map", 37 | "style", 38 | "tileset", 39 | "dataset", 40 | "search", 41 | "navigation" 42 | ], 43 | "author": "Mapbox", 44 | "license": "BSD-2-Clause", 45 | "bugs": { 46 | "url": "https://github.com/mapbox/mapbox-sdk-js/issues" 47 | }, 48 | "homepage": "https://github.com/mapbox/mapbox-sdk-js#readme", 49 | "browser": { 50 | "./lib/client.js": "./lib/browser/browser-client.js" 51 | }, 52 | "jest": { 53 | "transform": {}, 54 | "clearMocks": true, 55 | "testEnvironment": "node", 56 | "coverageReporters": [ 57 | "text", 58 | "html" 59 | ], 60 | "collectCoverageFrom": [ 61 | "/lib/**/*.js", 62 | "/services/**/*.js", 63 | "!/test/**" 64 | ] 65 | }, 66 | "lint-staged": { 67 | "*.js": [ 68 | "eslint", 69 | "prettier --write", 70 | "git add" 71 | ], 72 | "*.md": [ 73 | "remark-preset-davidtheclark", 74 | "git add" 75 | ] 76 | }, 77 | "prettier": { 78 | "singleQuote": true 79 | }, 80 | "dependencies": { 81 | "@mapbox/fusspot": "^0.4.0", 82 | "@mapbox/parse-mapbox-token": "^0.2.0", 83 | "@mapbox/polyline": "^1.0.0", 84 | "eventemitter3": "^3.1.0", 85 | "form-data": "^3.0.0", 86 | "got": "^11.8.5", 87 | "is-plain-obj": "^1.1.0", 88 | "xtend": "^4.0.1" 89 | }, 90 | "devDependencies": { 91 | "base64-url": "^2.2.0", 92 | "budo": "^11.8.4", 93 | "camelcase": "^5.0.0", 94 | "documentation": "^14.0.0", 95 | "eslint": "^5.1.0", 96 | "eslint-plugin-node": "^6.0.1", 97 | "express": "^4.16.3", 98 | "get-port": "^3.2.0", 99 | "husky": "^0.14.3", 100 | "jest": "^29.1.2", 101 | "jest-environment-jsdom": "^29.1.2", 102 | "jest-environment-jsdom-global": "^4.0.0", 103 | "lint-staged": "^7.2.0", 104 | "meow": "^10.1.5", 105 | "npm-run-all": "^4.1.3", 106 | "prettier": "^1.13.7", 107 | "remark-preset-davidtheclark": "^0.12.0", 108 | "rollup": "^0.62.0", 109 | "rollup-plugin-commonjs": "^9.1.3", 110 | "rollup-plugin-node-resolve": "^3.3.0", 111 | "uglify-js": "^3.4.4", 112 | "xhr-mock": "^2.4.1" 113 | }, 114 | "engines": { 115 | "node": ">=6" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | var path = require('path'); 5 | var commonjs = require('rollup-plugin-commonjs'); 6 | var nodeResolve = require('rollup-plugin-node-resolve'); 7 | 8 | module.exports = { 9 | input: path.join(__dirname, './bundle.js'), 10 | output: { 11 | file: path.join(__dirname, './umd/mapbox-sdk.js'), 12 | format: 'umd', 13 | name: 'mapboxSdk' 14 | }, 15 | plugins: [ 16 | nodeResolve({ 17 | browser: true 18 | }), 19 | commonjs() 20 | ] 21 | }; 22 | -------------------------------------------------------------------------------- /services/__tests__/datasets.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const datasetsService = require('../datasets'); 4 | const tu = require('../../test/test-utils'); 5 | 6 | let datasets; 7 | beforeEach(() => { 8 | datasets = datasetsService(tu.mockClient()); 9 | }); 10 | 11 | describe('listDatasets', () => { 12 | test('works', () => { 13 | datasets.listDatasets(); 14 | expect(tu.requestConfig(datasets)).toEqual({ 15 | path: '/datasets/v1/:ownerId', 16 | method: 'GET', 17 | query: {} 18 | }); 19 | }); 20 | 21 | test('with properties', () => { 22 | datasets.listDatasets({ 23 | sortby: 'created' 24 | }); 25 | expect(tu.requestConfig(datasets)).toEqual({ 26 | path: '/datasets/v1/:ownerId', 27 | method: 'GET', 28 | query: { 29 | sortby: 'created' 30 | } 31 | }); 32 | }); 33 | }); 34 | 35 | describe('createDataset', () => { 36 | test('works', () => { 37 | datasets.createDataset(); 38 | expect(tu.requestConfig(datasets)).toEqual({ 39 | path: '/datasets/v1/:ownerId', 40 | method: 'POST' 41 | }); 42 | }); 43 | 44 | test('with properties', () => { 45 | datasets.createDataset({ 46 | name: 'mock-name', 47 | description: 'mock-description' 48 | }); 49 | expect(tu.requestConfig(datasets)).toEqual({ 50 | path: '/datasets/v1/:ownerId', 51 | method: 'POST', 52 | body: { 53 | name: 'mock-name', 54 | description: 'mock-description' 55 | } 56 | }); 57 | }); 58 | }); 59 | 60 | describe('getMetadata', () => { 61 | test('works', () => { 62 | datasets.getMetadata({ datasetId: 'mock-dataset' }); 63 | expect(tu.requestConfig(datasets)).toEqual({ 64 | path: '/datasets/v1/:ownerId/:datasetId', 65 | method: 'GET', 66 | params: { 67 | datasetId: 'mock-dataset' 68 | } 69 | }); 70 | }); 71 | }); 72 | 73 | describe('updateMetadata', () => { 74 | test('works', () => { 75 | datasets.updateMetadata({ datasetId: 'mock-dataset' }); 76 | expect(tu.requestConfig(datasets)).toEqual({ 77 | path: '/datasets/v1/:ownerId/:datasetId', 78 | method: 'PATCH', 79 | params: { 80 | datasetId: 'mock-dataset' 81 | }, 82 | body: {} 83 | }); 84 | }); 85 | 86 | test('with properties', () => { 87 | datasets.updateMetadata({ 88 | datasetId: 'mock-dataset', 89 | name: 'mock-name', 90 | description: 'mock-description' 91 | }); 92 | expect(tu.requestConfig(datasets)).toEqual({ 93 | path: '/datasets/v1/:ownerId/:datasetId', 94 | method: 'PATCH', 95 | params: { 96 | datasetId: 'mock-dataset' 97 | }, 98 | body: { name: 'mock-name', description: 'mock-description' } 99 | }); 100 | }); 101 | }); 102 | 103 | describe('deleteDataset', () => { 104 | test('works', () => { 105 | datasets.deleteDataset({ datasetId: 'mock-dataset' }); 106 | expect(tu.requestConfig(datasets)).toEqual({ 107 | path: '/datasets/v1/:ownerId/:datasetId', 108 | method: 'DELETE', 109 | params: { 110 | datasetId: 'mock-dataset' 111 | } 112 | }); 113 | }); 114 | }); 115 | 116 | describe('listFeatures', () => { 117 | test('works', () => { 118 | datasets.listFeatures({ datasetId: 'mock-dataset' }); 119 | expect(tu.requestConfig(datasets)).toEqual({ 120 | path: '/datasets/v1/:ownerId/:datasetId/features', 121 | method: 'GET', 122 | params: { 123 | datasetId: 'mock-dataset' 124 | }, 125 | query: {} 126 | }); 127 | }); 128 | 129 | test('with limit and start', () => { 130 | datasets.listFeatures({ 131 | datasetId: 'mock-dataset', 132 | limit: 100, 133 | start: 'mock-feature' 134 | }); 135 | expect(tu.requestConfig(datasets)).toEqual({ 136 | path: '/datasets/v1/:ownerId/:datasetId/features', 137 | method: 'GET', 138 | params: { 139 | datasetId: 'mock-dataset' 140 | }, 141 | query: { limit: 100, start: 'mock-feature' } 142 | }); 143 | }); 144 | }); 145 | 146 | describe('putFeature', () => { 147 | test('updates an existing feature with partial data', () => { 148 | datasets.putFeature({ 149 | datasetId: 'mock-dataset', 150 | featureId: 'mock-feature', 151 | feature: { geometry: { type: 'Point', coordinates: [0, 10] } } 152 | }); 153 | expect(tu.requestConfig(datasets)).toEqual({ 154 | path: '/datasets/v1/:ownerId/:datasetId/features/:featureId', 155 | method: 'PUT', 156 | params: { 157 | datasetId: 'mock-dataset', 158 | featureId: 'mock-feature' 159 | }, 160 | body: { geometry: { type: 'Point', coordinates: [0, 10] } } 161 | }); 162 | }); 163 | 164 | test('creates a brand new feature', () => { 165 | datasets.putFeature({ 166 | datasetId: 'mock-dataset', 167 | featureId: 'mock-feature', 168 | feature: { 169 | type: 'Feature', 170 | geometry: { type: 'Point', coordinates: [0, 10] } 171 | } 172 | }); 173 | expect(tu.requestConfig(datasets)).toEqual({ 174 | path: '/datasets/v1/:ownerId/:datasetId/features/:featureId', 175 | method: 'PUT', 176 | params: { 177 | datasetId: 'mock-dataset', 178 | featureId: 'mock-feature' 179 | }, 180 | body: { 181 | type: 'Feature', 182 | geometry: { type: 'Point', coordinates: [0, 10] } 183 | } 184 | }); 185 | }); 186 | 187 | test('errors if feature ID does not match featureId in config', () => { 188 | tu.expectError( 189 | () => { 190 | datasets.putFeature({ 191 | datasetId: 'mock-dataset', 192 | featureId: 'mock-feature', 193 | feature: { 194 | type: 'Feature', 195 | id: 'foo', 196 | geometry: { type: 'Point', coordinates: [0, 10] } 197 | } 198 | }); 199 | }, 200 | error => { 201 | expect(error.message).toMatch( 202 | 'featureId must match the id property of the feature' 203 | ); 204 | } 205 | ); 206 | }); 207 | }); 208 | 209 | describe('getFeature', () => { 210 | test('works', () => { 211 | datasets.getFeature({ datasetId: 'mock-dataset', featureId: 'foo' }); 212 | expect(tu.requestConfig(datasets)).toEqual({ 213 | path: '/datasets/v1/:ownerId/:datasetId/features/:featureId', 214 | method: 'GET', 215 | params: { 216 | datasetId: 'mock-dataset', 217 | featureId: 'foo' 218 | } 219 | }); 220 | }); 221 | }); 222 | 223 | describe('deleteFeature', () => { 224 | test('works', () => { 225 | datasets.deleteFeature({ datasetId: 'mock-dataset', featureId: 'foo' }); 226 | expect(tu.requestConfig(datasets)).toEqual({ 227 | path: '/datasets/v1/:ownerId/:datasetId/features/:featureId', 228 | method: 'DELETE', 229 | params: { 230 | datasetId: 'mock-dataset', 231 | featureId: 'foo' 232 | } 233 | }); 234 | }); 235 | }); 236 | -------------------------------------------------------------------------------- /services/__tests__/directions.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const directionsService = require('../directions'); 4 | const tu = require('../../test/test-utils'); 5 | 6 | let directions; 7 | beforeEach(() => { 8 | directions = directionsService(tu.mockClient()); 9 | }); 10 | 11 | describe('getDirections', () => { 12 | test('works', () => { 13 | directions.getDirections({ 14 | waypoints: [ 15 | { 16 | coordinates: [2.2, 1.1] 17 | }, 18 | { 19 | coordinates: [2.2, 1.1] 20 | } 21 | ] 22 | }); 23 | expect(tu.requestConfig(directions)).toEqual({ 24 | path: '/directions/v5/mapbox/:profile/:coordinates', 25 | method: 'GET', 26 | params: { 27 | coordinates: '2.2,1.1;2.2,1.1', 28 | profile: 'driving' 29 | }, 30 | query: {} 31 | }); 32 | }); 33 | 34 | test('it omits queries not supplied', () => { 35 | directions.getDirections({ 36 | waypoints: [ 37 | { 38 | coordinates: [2.2, 1.1] 39 | }, 40 | { 41 | coordinates: [2.2, 1.1] 42 | } 43 | ], 44 | profile: 'walking', 45 | alternatives: false, 46 | geometries: 'polyline' 47 | }); 48 | expect(tu.requestConfig(directions)).toEqual({ 49 | path: '/directions/v5/mapbox/:profile/:coordinates', 50 | method: 'GET', 51 | params: { 52 | coordinates: '2.2,1.1;2.2,1.1', 53 | profile: 'walking' 54 | }, 55 | query: { 56 | alternatives: 'false', 57 | geometries: 'polyline' 58 | } 59 | }); 60 | }); 61 | 62 | test('it reads waypoints props', () => { 63 | directions.getDirections({ 64 | waypoints: [ 65 | { 66 | coordinates: [2.2, 1.1], 67 | radius: 2000, 68 | bearing: [45, 20] 69 | }, 70 | { 71 | coordinates: [2.2, 1.1], 72 | radius: 2000, 73 | bearing: [46, 21] 74 | } 75 | ], 76 | profile: 'walking', 77 | steps: false, 78 | continueStraight: false 79 | }); 80 | expect(tu.requestConfig(directions)).toEqual({ 81 | path: '/directions/v5/mapbox/:profile/:coordinates', 82 | method: 'GET', 83 | params: { 84 | coordinates: '2.2,1.1;2.2,1.1', 85 | profile: 'walking' 86 | }, 87 | query: { 88 | steps: 'false', 89 | continue_straight: 'false', 90 | radiuses: '2000;2000', 91 | bearings: '45,20;46,21' 92 | } 93 | }); 94 | }); 95 | 96 | test('it works if an optional waypoints.bearing is missing at some places', () => { 97 | directions.getDirections({ 98 | waypoints: [ 99 | { 100 | coordinates: [2.2, 1.1] 101 | }, 102 | { 103 | coordinates: [2.2, 1.1] 104 | }, 105 | { 106 | coordinates: [2.2, 1.1], 107 | bearing: [45, 32] 108 | }, 109 | { 110 | coordinates: [2.2, 1.1] 111 | } 112 | ], 113 | profile: 'walking', 114 | steps: false, 115 | continueStraight: false 116 | }); 117 | expect(tu.requestConfig(directions)).toEqual({ 118 | path: '/directions/v5/mapbox/:profile/:coordinates', 119 | method: 'GET', 120 | params: { 121 | coordinates: '2.2,1.1;2.2,1.1;2.2,1.1;2.2,1.1', 122 | profile: 'walking' 123 | }, 124 | query: { 125 | steps: 'false', 126 | continue_straight: 'false', 127 | bearings: ';;45,32;' 128 | } 129 | }); 130 | }); 131 | 132 | test('it works if an optional waypoints.radius is missing at some places', () => { 133 | directions.getDirections({ 134 | waypoints: [ 135 | { 136 | coordinates: [2.2, 1.1], 137 | radius: 2000 138 | }, 139 | { 140 | coordinates: [2.2, 1.1] 141 | }, 142 | { 143 | coordinates: [2.2, 1.1], 144 | bearing: [45, 32] 145 | }, 146 | { 147 | coordinates: [2.2, 1.1] 148 | } 149 | ], 150 | profile: 'walking', 151 | steps: false, 152 | continueStraight: false 153 | }); 154 | expect(tu.requestConfig(directions)).toEqual({ 155 | path: '/directions/v5/mapbox/:profile/:coordinates', 156 | method: 'GET', 157 | params: { 158 | coordinates: '2.2,1.1;2.2,1.1;2.2,1.1;2.2,1.1', 159 | profile: 'walking' 160 | }, 161 | query: { 162 | steps: 'false', 163 | continue_straight: 'false', 164 | bearings: ';;45,32;', 165 | radiuses: '2000;;;' 166 | } 167 | }); 168 | }); 169 | 170 | test('waypoints.radius can be any of string or number', () => { 171 | directions.getDirections({ 172 | waypoints: [ 173 | { 174 | coordinates: [2.2, 1.1], 175 | radius: 2000 176 | }, 177 | { 178 | coordinates: [2.2, 1.1], 179 | radius: 'unlimited' 180 | } 181 | ] 182 | }); 183 | 184 | expect(tu.requestConfig(directions)).toEqual({ 185 | path: '/directions/v5/mapbox/:profile/:coordinates', 186 | method: 'GET', 187 | params: { 188 | coordinates: '2.2,1.1;2.2,1.1', 189 | profile: 'driving' 190 | }, 191 | query: { 192 | radiuses: '2000;unlimited' 193 | } 194 | }); 195 | }); 196 | 197 | test('errors if there are too few waypoints', () => { 198 | expect(() => { 199 | directions.getDirections({ 200 | waypoints: [ 201 | { 202 | coordinates: [2.2, 1.1], 203 | radius: 2000 204 | } 205 | ] 206 | }); 207 | }).toThrowError(/between 2 and 25/); 208 | }); 209 | 210 | test('errors if there are too many waypoints', () => { 211 | expect(() => { 212 | const waypoints = []; 213 | for (let i = 0; i < 26; i++) { 214 | waypoints.push({ 215 | coordinates: [2.2, 1.1], 216 | radius: 2000 217 | }); 218 | } 219 | directions.getDirections({ 220 | waypoints 221 | }); 222 | }).toThrowError(/between 2 and 25/); 223 | }); 224 | }); 225 | -------------------------------------------------------------------------------- /services/__tests__/geocoding-v6.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const geocodingService = require('../geocoding-v6'); 4 | const tu = require('../../test/test-utils'); 5 | 6 | let geocoding; 7 | beforeEach(() => { 8 | geocoding = geocodingService(tu.mockClient()); 9 | }); 10 | 11 | describe('forwardGeocode', () => { 12 | test('with minimal config', () => { 13 | geocoding.forwardGeocode({ 14 | query: 'Tucson' 15 | }); 16 | expect(tu.requestConfig(geocoding)).toEqual({ 17 | method: 'GET', 18 | path: '/search/geocode/v6/forward', 19 | query: { q: 'Tucson' } 20 | }); 21 | }); 22 | 23 | test('standard mode with all config options', () => { 24 | geocoding.forwardGeocode({ 25 | query: 'Tucson', 26 | mode: 'standard', 27 | countries: ['AO', 'AR'], 28 | proximity: [3, 4], 29 | types: ['street', 'country', 'region', 'address', 'secondary_address'], 30 | autocomplete: true, 31 | bbox: [1, 2, 3, 4], 32 | format: 'v5', 33 | limit: 3, 34 | language: 'de', 35 | worldview: 'us', 36 | permanent: true, 37 | session_token: 'abc123', 38 | 39 | // structured input parameters will be ignored in normal mode 40 | address_line1: '12 main', 41 | address_number: '12', 42 | street: 'Main', 43 | block: 'block', 44 | place: 'some place', 45 | region: 'region', 46 | neighborhood: 'neighborhood', 47 | postcode: '1234', 48 | locality: 'locality' 49 | }); 50 | expect(tu.requestConfig(geocoding)).toEqual({ 51 | method: 'GET', 52 | path: '/search/geocode/v6/forward', 53 | query: { 54 | q: 'Tucson', 55 | country: ['AO', 'AR'], 56 | proximity: [3, 4], 57 | types: ['street', 'country', 'region', 'address', 'secondary_address'], 58 | autocomplete: 'true', 59 | bbox: [1, 2, 3, 4], 60 | format: 'v5', 61 | limit: 3, 62 | language: 'de', 63 | worldview: 'us', 64 | permanent: 'true', 65 | session_token: 'abc123' 66 | } 67 | }); 68 | }); 69 | 70 | test('structured input mode with all config options', () => { 71 | geocoding.forwardGeocode({ 72 | mode: 'structured', 73 | countries: 'AO', 74 | proximity: [3, 4], 75 | types: ['street', 'country', 'region'], 76 | autocomplete: true, 77 | bbox: [1, 2, 3, 4], 78 | limit: 3, 79 | language: 'de', 80 | worldview: 'us', 81 | session_token: 'abc123', 82 | 83 | // structured input parameters will be picked 84 | address_line1: '12 main', 85 | address_number: '12', 86 | street: 'Main', 87 | block: 'block', 88 | place: 'some place', 89 | region: 'region', 90 | neighborhood: 'neighborhood', 91 | postcode: '1234', 92 | locality: 'locality' 93 | }); 94 | expect(tu.requestConfig(geocoding)).toEqual({ 95 | method: 'GET', 96 | path: '/search/geocode/v6/forward', 97 | query: { 98 | proximity: [3, 4], 99 | types: ['street', 'country', 'region'], 100 | autocomplete: 'true', 101 | bbox: [1, 2, 3, 4], 102 | limit: 3, 103 | language: 'de', 104 | worldview: 'us', 105 | session_token: 'abc123', 106 | 107 | address_line1: '12 main', 108 | address_number: '12', 109 | street: 'Main', 110 | block: 'block', 111 | place: 'some place', 112 | region: 'region', 113 | neighborhood: 'neighborhood', 114 | country: 'AO', 115 | postcode: '1234', 116 | locality: 'locality' 117 | } 118 | }); 119 | }); 120 | }); 121 | 122 | describe('reverseGeocode', () => { 123 | test('with minimal config', () => { 124 | geocoding.reverseGeocode({ 125 | longitude: 15, 126 | latitude: 14 127 | }); 128 | expect(tu.requestConfig(geocoding)).toEqual({ 129 | method: 'GET', 130 | path: '/search/geocode/v6/reverse', 131 | query: { 132 | longitude: 15, 133 | latitude: 14 134 | } 135 | }); 136 | }); 137 | 138 | test('with all config options', () => { 139 | geocoding.reverseGeocode({ 140 | longitude: 15, 141 | latitude: 14, 142 | countries: ['AO', 'AR'], 143 | types: ['country', 'region'], 144 | limit: 3, 145 | language: 'de', 146 | worldview: 'us', 147 | permanent: true, 148 | session_token: 'abc123' 149 | }); 150 | expect(tu.requestConfig(geocoding)).toEqual({ 151 | method: 'GET', 152 | path: '/search/geocode/v6/reverse', 153 | query: { 154 | longitude: 15, 155 | latitude: 14, 156 | country: ['AO', 'AR'], 157 | types: ['country', 'region'], 158 | limit: 3, 159 | language: 'de', 160 | worldview: 'us', 161 | permanent: 'true', 162 | session_token: 'abc123' 163 | } 164 | }); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /services/__tests__/geocoding.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const geocodingService = require('../geocoding'); 4 | const tu = require('../../test/test-utils'); 5 | 6 | let geocoding; 7 | beforeEach(() => { 8 | geocoding = geocodingService(tu.mockClient()); 9 | }); 10 | 11 | describe('forwardGeocode', () => { 12 | test('with minimal config', () => { 13 | geocoding.forwardGeocode({ 14 | query: 'Tucson' 15 | }); 16 | expect(tu.requestConfig(geocoding)).toEqual({ 17 | method: 'GET', 18 | path: '/geocoding/v5/:mode/:query.json', 19 | params: { 20 | query: 'Tucson', 21 | mode: 'mapbox.places' 22 | }, 23 | query: {} 24 | }); 25 | }); 26 | 27 | test('with all config options', () => { 28 | geocoding.forwardGeocode({ 29 | query: 'Tucson', 30 | mode: 'mapbox.places-permanent', 31 | countries: ['AO', 'AR'], 32 | proximity: [3, 4], 33 | types: ['country', 'region'], 34 | autocomplete: true, 35 | bbox: [1, 2, 3, 4], 36 | limit: 3, 37 | language: ['de', 'bs'], 38 | routing: true, 39 | fuzzyMatch: true, 40 | worldview: 'us', 41 | session_token: 'abc123' 42 | }); 43 | expect(tu.requestConfig(geocoding)).toEqual({ 44 | method: 'GET', 45 | path: '/geocoding/v5/:mode/:query.json', 46 | params: { 47 | query: 'Tucson', 48 | mode: 'mapbox.places-permanent' 49 | }, 50 | query: { 51 | country: ['AO', 'AR'], 52 | proximity: [3, 4], 53 | types: ['country', 'region'], 54 | autocomplete: 'true', 55 | bbox: [1, 2, 3, 4], 56 | limit: 3, 57 | language: ['de', 'bs'], 58 | routing: 'true', 59 | fuzzyMatch: 'true', 60 | worldview: 'us', 61 | session_token: 'abc123' 62 | } 63 | }); 64 | }); 65 | }); 66 | 67 | describe('reverseGeocode', () => { 68 | test('with minimal config', () => { 69 | geocoding.reverseGeocode({ 70 | query: [15, 14] 71 | }); 72 | expect(tu.requestConfig(geocoding)).toEqual({ 73 | method: 'GET', 74 | path: '/geocoding/v5/:mode/:query.json', 75 | params: { 76 | query: [15, 14], 77 | mode: 'mapbox.places' 78 | }, 79 | query: {} 80 | }); 81 | }); 82 | 83 | test('with all config options', () => { 84 | geocoding.reverseGeocode({ 85 | query: [15, 14], 86 | mode: 'mapbox.places-permanent', 87 | countries: ['AO', 'AR'], 88 | types: ['country', 'region'], 89 | bbox: [1, 2, 3, 4], 90 | limit: 3, 91 | language: ['de', 'bs'], 92 | reverseMode: 'distance', 93 | routing: true, 94 | worldview: 'us', 95 | session_token: 'abc123' 96 | }); 97 | expect(tu.requestConfig(geocoding)).toEqual({ 98 | method: 'GET', 99 | path: '/geocoding/v5/:mode/:query.json', 100 | params: { 101 | query: [15, 14], 102 | mode: 'mapbox.places-permanent' 103 | }, 104 | query: { 105 | country: ['AO', 'AR'], 106 | types: ['country', 'region'], 107 | bbox: [1, 2, 3, 4], 108 | limit: 3, 109 | language: ['de', 'bs'], 110 | reverseMode: 'distance', 111 | routing: 'true', 112 | worldview: 'us', 113 | session_token: 'abc123' 114 | } 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /services/__tests__/isochrone.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isochroneService = require('../isochrone'); 4 | const tu = require('../../test/test-utils'); 5 | 6 | let isochrone; 7 | 8 | beforeEach(() => { 9 | isochrone = isochroneService(tu.mockClient()); 10 | }); 11 | 12 | describe('getContours', () => { 13 | test('works', () => { 14 | isochrone.getContours({ 15 | coordinates: [-118.22258, 33.99038], 16 | minutes: [5, 10, 15] 17 | }); 18 | 19 | expect(tu.requestConfig(isochrone)).toEqual({ 20 | path: '/isochrone/v1/mapbox/:profile/:coordinates', 21 | method: 'GET', 22 | params: { 23 | coordinates: '-118.22258,33.99038', 24 | profile: 'driving' 25 | }, 26 | query: { 27 | contours_minutes: '5,10,15' 28 | } 29 | }); 30 | }); 31 | 32 | test('it omits queries not supplied', () => { 33 | isochrone.getContours({ 34 | coordinates: [-118.22258, 33.99038], 35 | minutes: [5, 10, 15], 36 | profile: 'walking', 37 | polygons: true 38 | }); 39 | 40 | expect(tu.requestConfig(isochrone)).toEqual({ 41 | path: '/isochrone/v1/mapbox/:profile/:coordinates', 42 | method: 'GET', 43 | params: { 44 | coordinates: '-118.22258,33.99038', 45 | profile: 'walking' 46 | }, 47 | query: { 48 | contours_minutes: '5,10,15', 49 | polygons: 'true' 50 | } 51 | }); 52 | }); 53 | 54 | test('with all config options (minutes)', () => { 55 | isochrone.getContours({ 56 | coordinates: [-118.22258, 33.99038], 57 | minutes: [5, 10, 15, 20], 58 | profile: 'walking', 59 | polygons: true, 60 | colors: ['6706ce', '04e813', '4286f4', '555555'], 61 | denoise: 0, 62 | generalize: 0 63 | }); 64 | 65 | expect(tu.requestConfig(isochrone)).toEqual({ 66 | path: '/isochrone/v1/mapbox/:profile/:coordinates', 67 | method: 'GET', 68 | params: { 69 | coordinates: '-118.22258,33.99038', 70 | profile: 'walking' 71 | }, 72 | query: { 73 | contours_minutes: '5,10,15,20', 74 | polygons: 'true', 75 | contours_colors: '6706ce,04e813,4286f4,555555', 76 | denoise: 0, 77 | generalize: 0 78 | } 79 | }); 80 | }); 81 | 82 | test('with all config options (meters)', () => { 83 | isochrone.getContours({ 84 | coordinates: [-118.22258, 33.99038], 85 | meters: [5000, 10000, 15000, 20000], 86 | profile: 'walking', 87 | polygons: true, 88 | colors: ['6706ce', '04e813', '4286f4', '555555'], 89 | denoise: 0, 90 | generalize: 0 91 | }); 92 | 93 | expect(tu.requestConfig(isochrone)).toEqual({ 94 | path: '/isochrone/v1/mapbox/:profile/:coordinates', 95 | method: 'GET', 96 | params: { 97 | coordinates: '-118.22258,33.99038', 98 | profile: 'walking' 99 | }, 100 | query: { 101 | contours_meters: '5000,10000,15000,20000', 102 | polygons: 'true', 103 | contours_colors: '6706ce,04e813,4286f4,555555', 104 | denoise: 0, 105 | generalize: 0 106 | } 107 | }); 108 | }); 109 | 110 | test('hex colors with # are removed', () => { 111 | isochrone.getContours({ 112 | coordinates: [-118.22258, 33.99038], 113 | minutes: [5, 10, 15], 114 | colors: ['#6706ce', '#04e813', '#4286f4'] 115 | }); 116 | 117 | expect(tu.requestConfig(isochrone)).toEqual({ 118 | path: '/isochrone/v1/mapbox/:profile/:coordinates', 119 | method: 'GET', 120 | params: { 121 | coordinates: '-118.22258,33.99038', 122 | profile: 'driving' 123 | }, 124 | query: { 125 | contours_minutes: '5,10,15', 126 | contours_colors: '6706ce,04e813,4286f4' 127 | } 128 | }); 129 | }); 130 | 131 | test('errors if more than 4 contours_minutes requested', () => { 132 | expect(() => 133 | isochrone.getContours({ 134 | coordinates: [-118.22258, 33.99038], 135 | minutes: [5, 10, 15, 20, 30] 136 | }) 137 | ).toThrow('minutes must contain between 1 and 4 contour values'); 138 | }); 139 | 140 | test('errors if more than 4 contours_meters requested', () => { 141 | expect(() => 142 | isochrone.getContours({ 143 | coordinates: [-118.22258, 33.99038], 144 | meters: [5000, 10000, 15000, 20000, 30000] 145 | }) 146 | ).toThrow('meters must contain between 1 and 4 contour values'); 147 | }); 148 | 149 | test('errors if minute value is greater than 60', () => { 150 | expect(() => 151 | isochrone.getContours({ 152 | coordinates: [-118.22258, 33.99038], 153 | minutes: [40, 50, 60, 70] 154 | }) 155 | ).toThrow('minutes must be less than 60'); 156 | }); 157 | 158 | test('errors if meter value is greater than 100000', () => { 159 | expect(() => 160 | isochrone.getContours({ 161 | coordinates: [-118.22258, 33.99038], 162 | meters: [10000, 50000, 100000, 100001] 163 | }) 164 | ).toThrow('meters must be less than 100000'); 165 | }); 166 | 167 | test('errors if generalize is less than 0', () => { 168 | expect(() => 169 | isochrone.getContours({ 170 | coordinates: [-118.22258, 33.99038], 171 | minutes: [40, 50, 60], 172 | generalize: -1 173 | }) 174 | ).toThrow('generalize tolerance must be a positive number'); 175 | }); 176 | 177 | test('colors should have the same number of entries as minutes', () => { 178 | expect(() => 179 | isochrone.getContours({ 180 | coordinates: [-118.22258, 33.99038], 181 | minutes: [5, 10, 15, 20], 182 | profile: 'walking', 183 | colors: ['6706ce', '04e813'] 184 | }) 185 | ).toThrow('colors should have the same number of entries as minutes'); 186 | }); 187 | 188 | test('colors should have the same number of entries as meters', () => { 189 | expect(() => 190 | isochrone.getContours({ 191 | coordinates: [-118.22258, 33.99038], 192 | meters: [5000, 10000, 15000, 20000], 193 | profile: 'walking', 194 | colors: ['6706ce', '04e813'] 195 | }) 196 | ).toThrow('colors should have the same number of entries as meters'); 197 | }); 198 | 199 | test('either meters or minutes should be specified', () => { 200 | expect(() => 201 | isochrone.getContours({ 202 | coordinates: [-118.22258, 33.99038], 203 | minutes: [5, 10, 15, 20], 204 | meters: [5000, 10000, 15000, 20000], 205 | profile: 'walking' 206 | }) 207 | ).toThrow("minutes and meters can't be specified at the same time"); 208 | }); 209 | }); 210 | -------------------------------------------------------------------------------- /services/__tests__/map-matching.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mapMatchingService = require('../map-matching'); 4 | const tu = require('../../test/test-utils'); 5 | 6 | let mapMatching; 7 | beforeEach(() => { 8 | mapMatching = mapMatchingService(tu.mockClient()); 9 | }); 10 | 11 | function urlEncodeBody(body) { 12 | return body 13 | .map(([key, val]) => `${key}=${encodeURIComponent(val)}`) 14 | .join('&'); 15 | } 16 | 17 | describe('getMatch', () => { 18 | test('works', () => { 19 | mapMatching.getMatch({ 20 | points: [ 21 | { 22 | coordinates: [2.2, 1.1] 23 | }, 24 | { 25 | coordinates: [2.2, 1.1] 26 | } 27 | ] 28 | }); 29 | expect(tu.requestConfig(mapMatching)).toEqual({ 30 | path: '/matching/v5/mapbox/:profile', 31 | method: 'POST', 32 | body: urlEncodeBody([['coordinates', '2.2,1.1;2.2,1.1']]), 33 | headers: { 'content-type': 'application/x-www-form-urlencoded' }, 34 | params: { profile: 'driving' } 35 | }); 36 | }); 37 | 38 | test('it understands isWaypoint', () => { 39 | mapMatching.getMatch({ 40 | points: [ 41 | { 42 | coordinates: [2.2, 1.1] 43 | }, 44 | { 45 | coordinates: [2.2, 1.1], 46 | isWaypoint: false 47 | }, 48 | { 49 | coordinates: [3.2, 1.1] 50 | }, 51 | { 52 | coordinates: [4.2, 1.1] 53 | } 54 | ], 55 | profile: 'walking', 56 | tidy: true, 57 | geometries: 'polyline6' 58 | }); 59 | expect(tu.requestConfig(mapMatching)).toEqual({ 60 | path: '/matching/v5/mapbox/:profile', 61 | method: 'POST', 62 | body: urlEncodeBody([ 63 | ['geometries', 'polyline6'], 64 | ['tidy', 'true'], 65 | ['waypoints', '0;2;3'], 66 | ['coordinates', '2.2,1.1;2.2,1.1;3.2,1.1;4.2,1.1'] 67 | ]), 68 | headers: { 'content-type': 'application/x-www-form-urlencoded' }, 69 | params: { profile: 'walking' } 70 | }); 71 | }); 72 | 73 | test('it omits waypoints if all isWaypoints are true', () => { 74 | mapMatching.getMatch({ 75 | points: [ 76 | { 77 | coordinates: [2.2, 1.1], 78 | isWaypoint: true 79 | }, 80 | { 81 | coordinates: [2.2, 1.1], 82 | isWaypoint: true 83 | }, 84 | { 85 | coordinates: [3.2, 1.1], 86 | isWaypoint: true 87 | }, 88 | { 89 | coordinates: [4.2, 1.1], 90 | isWaypoint: true 91 | } 92 | ], 93 | profile: 'walking', 94 | steps: false 95 | }); 96 | expect(tu.requestConfig(mapMatching)).toEqual({ 97 | path: '/matching/v5/mapbox/:profile', 98 | method: 'POST', 99 | body: urlEncodeBody([ 100 | ['steps', 'false'], 101 | ['coordinates', '2.2,1.1;2.2,1.1;3.2,1.1;4.2,1.1'] 102 | ]), 103 | headers: { 'content-type': 'application/x-www-form-urlencoded' }, 104 | params: { profile: 'walking' } 105 | }); 106 | }); 107 | 108 | test('it always keeps first and last waypoint', () => { 109 | mapMatching.getMatch({ 110 | points: [ 111 | { 112 | coordinates: [2.2, 1.1], 113 | isWaypoint: false 114 | }, 115 | { 116 | coordinates: [2.2, 1.1], 117 | isWaypoint: false 118 | }, 119 | { 120 | coordinates: [3.2, 1.1], 121 | isWaypoint: false 122 | }, 123 | { 124 | coordinates: [4.2, 1.1], 125 | isWaypoint: false 126 | } 127 | ], 128 | profile: 'walking', 129 | steps: false 130 | }); 131 | expect(tu.requestConfig(mapMatching)).toEqual({ 132 | path: '/matching/v5/mapbox/:profile', 133 | method: 'POST', 134 | body: urlEncodeBody([ 135 | ['steps', 'false'], 136 | ['waypoints', '0;3'], 137 | ['coordinates', '2.2,1.1;2.2,1.1;3.2,1.1;4.2,1.1'] 138 | ]), 139 | headers: { 'content-type': 'application/x-www-form-urlencoded' }, 140 | params: { profile: 'walking' } 141 | }); 142 | }); 143 | 144 | test('it understands other coordinate properties', () => { 145 | mapMatching.getMatch({ 146 | points: [ 147 | { 148 | coordinates: [2.2, 1.1] 149 | }, 150 | { 151 | coordinates: [2.2, 1.1], 152 | approach: 'curb', 153 | isWaypoint: true 154 | }, 155 | { 156 | coordinates: [2.2, 1.1], 157 | timestamp: 1528157886576, 158 | waypointName: 'special', 159 | radius: 50, 160 | isWaypoint: false 161 | }, 162 | { 163 | coordinates: [2.2, 1.1], 164 | timestamp: new Date(1528157886888), 165 | approach: 'unrestricted' 166 | } 167 | ], 168 | profile: 'walking', 169 | steps: true 170 | }); 171 | expect(tu.requestConfig(mapMatching)).toEqual({ 172 | path: '/matching/v5/mapbox/:profile', 173 | method: 'POST', 174 | params: { 175 | profile: 'walking' 176 | }, 177 | headers: { 'content-type': 'application/x-www-form-urlencoded' }, 178 | body: urlEncodeBody([ 179 | ['steps', 'true'], 180 | ['approaches', ';curb;;unrestricted'], 181 | ['radiuses', ';;50;'], 182 | ['waypoints', '0;1;3'], 183 | ['timestamps', ';;1528157886576;1528157886888'], 184 | ['waypoint_names', ';;special;'], 185 | ['coordinates', '2.2,1.1;2.2,1.1;2.2,1.1;2.2,1.1'] 186 | ]) 187 | }); 188 | }); 189 | 190 | test('errors if there are too few points', () => { 191 | expect(() => { 192 | mapMatching.getMatch({ 193 | points: [{ coordinates: [2.2, 1.1] }] 194 | }); 195 | }).toThrowError(/between 2 and 100/); 196 | }); 197 | 198 | test('errors if there are too many points', () => { 199 | expect(() => { 200 | const points = []; 201 | for (let i = 0; i < 101; i++) { 202 | points.push({ coordinates: [2.2, 1.1] }); 203 | } 204 | mapMatching.getMatch({ 205 | points 206 | }); 207 | }).toThrowError(/between 2 and 100/); 208 | }); 209 | }); 210 | -------------------------------------------------------------------------------- /services/__tests__/matrix.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const matrixService = require('../matrix'); 4 | const tu = require('../../test/test-utils'); 5 | 6 | let matrix; 7 | beforeEach(() => { 8 | matrix = matrixService(tu.mockClient()); 9 | }); 10 | 11 | describe('getMatrix', () => { 12 | test('works', () => { 13 | matrix.getMatrix({ 14 | points: [ 15 | { 16 | coordinates: [2.2, 1.1] 17 | }, 18 | { 19 | coordinates: [2.2, 1.1] 20 | } 21 | ] 22 | }); 23 | expect(tu.requestConfig(matrix)).toEqual({ 24 | path: '/directions-matrix/v1/mapbox/:profile/:coordinates', 25 | method: 'GET', 26 | params: { 27 | coordinates: '2.2,1.1;2.2,1.1', 28 | profile: 'driving' 29 | }, 30 | query: {} 31 | }); 32 | }); 33 | 34 | test('it understands approach', () => { 35 | matrix.getMatrix({ 36 | points: [ 37 | { 38 | coordinates: [2.2, 1.1] 39 | }, 40 | { 41 | coordinates: [2.2, 1.1], 42 | approach: 'curb' 43 | }, 44 | { 45 | coordinates: [3.2, 1.1] 46 | }, 47 | { 48 | coordinates: [4.2, 1.1] 49 | } 50 | ], 51 | profile: 'walking' 52 | }); 53 | expect(tu.requestConfig(matrix)).toEqual({ 54 | path: '/directions-matrix/v1/mapbox/:profile/:coordinates', 55 | method: 'GET', 56 | params: { 57 | coordinates: '2.2,1.1;2.2,1.1;3.2,1.1;4.2,1.1', 58 | profile: 'walking' 59 | }, 60 | query: { 61 | approaches: ';curb;;' 62 | } 63 | }); 64 | }); 65 | 66 | test('understands annotations ', () => { 67 | matrix.getMatrix({ 68 | points: [ 69 | { 70 | coordinates: [2.2, 1.1] 71 | }, 72 | { 73 | coordinates: [2.2, 1.1] 74 | }, 75 | { 76 | coordinates: [3.2, 1.1] 77 | }, 78 | { 79 | coordinates: [4.2, 1.1] 80 | } 81 | ], 82 | profile: 'walking', 83 | annotations: ['distance', 'duration'] 84 | }); 85 | expect(tu.requestConfig(matrix)).toEqual({ 86 | path: '/directions-matrix/v1/mapbox/:profile/:coordinates', 87 | method: 'GET', 88 | params: { 89 | coordinates: '2.2,1.1;2.2,1.1;3.2,1.1;4.2,1.1', 90 | profile: 'walking' 91 | }, 92 | query: { 93 | annotations: 'distance,duration' 94 | } 95 | }); 96 | }); 97 | 98 | test('handles when sources & destinations are array', () => { 99 | matrix.getMatrix({ 100 | points: [ 101 | { 102 | coordinates: [2.2, 1.1] 103 | }, 104 | { 105 | coordinates: [2.2, 1.1] 106 | }, 107 | { 108 | coordinates: [3.2, 1.1] 109 | }, 110 | { 111 | coordinates: [4.2, 1.1] 112 | } 113 | ], 114 | profile: 'walking', 115 | sources: [1, 5], 116 | destinations: [0, 2] 117 | }); 118 | expect(tu.requestConfig(matrix)).toEqual({ 119 | path: '/directions-matrix/v1/mapbox/:profile/:coordinates', 120 | method: 'GET', 121 | params: { 122 | coordinates: '2.2,1.1;2.2,1.1;3.2,1.1;4.2,1.1', 123 | profile: 'walking' 124 | }, 125 | query: { 126 | destinations: '0;2', 127 | sources: '1;5' 128 | } 129 | }); 130 | }); 131 | 132 | test('handles when sources & destinations are `all`', () => { 133 | matrix.getMatrix({ 134 | points: [ 135 | { 136 | coordinates: [2.2, 1.1] 137 | }, 138 | { 139 | coordinates: [2.2, 1.1] 140 | }, 141 | { 142 | coordinates: [3.2, 1.1] 143 | }, 144 | { 145 | coordinates: [4.2, 1.1] 146 | } 147 | ], 148 | profile: 'walking', 149 | sources: 'all', 150 | destinations: 'all' 151 | }); 152 | expect(tu.requestConfig(matrix)).toEqual({ 153 | path: '/directions-matrix/v1/mapbox/:profile/:coordinates', 154 | method: 'GET', 155 | params: { 156 | coordinates: '2.2,1.1;2.2,1.1;3.2,1.1;4.2,1.1', 157 | profile: 'walking' 158 | }, 159 | query: { 160 | destinations: 'all', 161 | sources: 'all' 162 | } 163 | }); 164 | }); 165 | 166 | test('errors if there are too few points', () => { 167 | expect(() => { 168 | matrix.getMatrix({ 169 | points: [{ coordinates: [2.2, 1.1] }] 170 | }); 171 | }).toThrowError(/between 2 and 100/); 172 | }); 173 | 174 | test('errors if there are too many points', () => { 175 | expect(() => { 176 | const points = []; 177 | for (let i = 0; i < 101; i++) { 178 | points.push({ coordinates: [2.2, 1.1] }); 179 | } 180 | matrix.getMatrix({ 181 | points 182 | }); 183 | }).toThrowError(/between 2 and 100/); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /services/__tests__/optimization.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const optimizationService = require('../optimization'); 4 | const tu = require('../../test/test-utils'); 5 | 6 | let optimization; 7 | beforeEach(() => { 8 | optimization = optimizationService(tu.mockClient()); 9 | }); 10 | 11 | describe('getOptimization', () => { 12 | test('works', () => { 13 | optimization.getOptimization({ 14 | waypoints: [ 15 | { 16 | coordinates: [2.2, 1.1] 17 | }, 18 | { 19 | coordinates: [2.2, 1.1] 20 | } 21 | ] 22 | }); 23 | expect(tu.requestConfig(optimization)).toEqual({ 24 | path: '/optimized-trips/v1/mapbox/:profile/:coordinates', 25 | method: 'GET', 26 | params: { 27 | coordinates: '2.2,1.1;2.2,1.1', 28 | profile: 'driving' 29 | }, 30 | query: {} 31 | }); 32 | }); 33 | 34 | test('No queries are added that the user has not supplied', () => { 35 | optimization.getOptimization({ 36 | waypoints: [ 37 | { 38 | coordinates: [2.2, 1.1] 39 | }, 40 | { 41 | coordinates: [2.2, 1.1] 42 | } 43 | ], 44 | profile: 'walking', 45 | geometries: 'polyline' 46 | }); 47 | expect(tu.requestConfig(optimization)).toEqual({ 48 | path: '/optimized-trips/v1/mapbox/:profile/:coordinates', 49 | method: 'GET', 50 | params: { 51 | coordinates: '2.2,1.1;2.2,1.1', 52 | profile: 'walking' 53 | }, 54 | query: { 55 | geometries: 'polyline' 56 | } 57 | }); 58 | }); 59 | 60 | test('it reads waypoints props', () => { 61 | optimization.getOptimization({ 62 | waypoints: [ 63 | { 64 | coordinates: [2.2, 1.1], 65 | radius: 2000, 66 | bearing: [45, 20] 67 | }, 68 | { 69 | coordinates: [2.2, 1.1], 70 | radius: 2000, 71 | bearing: [46, 21] 72 | } 73 | ], 74 | profile: 'walking', 75 | steps: false 76 | }); 77 | expect(tu.requestConfig(optimization)).toEqual({ 78 | path: '/optimized-trips/v1/mapbox/:profile/:coordinates', 79 | method: 'GET', 80 | params: { 81 | coordinates: '2.2,1.1;2.2,1.1', 82 | profile: 'walking' 83 | }, 84 | query: { 85 | steps: 'false', 86 | radiuses: '2000;2000', 87 | bearings: '45,20;46,21' 88 | } 89 | }); 90 | }); 91 | 92 | test('errors if too few waypoints are provided', () => { 93 | tu.expectError( 94 | () => { 95 | optimization.getOptimization({ 96 | waypoints: [ 97 | { 98 | coordinates: [2.2, 1.1] 99 | } 100 | ], 101 | profile: 'walking' 102 | }); 103 | }, 104 | error => { 105 | expect(error.message).toMatch( 106 | 'waypoints must include at least 2 OptimizationWaypoints' 107 | ); 108 | } 109 | ); 110 | }); 111 | 112 | test('it works if an optional waypoints.bearing is missing at some places', () => { 113 | optimization.getOptimization({ 114 | waypoints: [ 115 | { 116 | coordinates: [2.2, 1.1] 117 | }, 118 | { 119 | coordinates: [2.2, 1.1] 120 | }, 121 | { 122 | coordinates: [2.2, 1.1], 123 | bearing: [45, 32] 124 | }, 125 | { 126 | coordinates: [2.2, 1.1] 127 | } 128 | ], 129 | profile: 'walking', 130 | steps: false 131 | }); 132 | expect(tu.requestConfig(optimization)).toEqual({ 133 | path: '/optimized-trips/v1/mapbox/:profile/:coordinates', 134 | method: 'GET', 135 | params: { 136 | coordinates: '2.2,1.1;2.2,1.1;2.2,1.1;2.2,1.1', 137 | profile: 'walking' 138 | }, 139 | query: { 140 | steps: 'false', 141 | bearings: ';;45,32;' 142 | } 143 | }); 144 | }); 145 | 146 | test('it works if an optional waypoints.radius is missing at some places', () => { 147 | optimization.getOptimization({ 148 | waypoints: [ 149 | { 150 | coordinates: [2.2, 1.1], 151 | radius: 2000 152 | }, 153 | { 154 | coordinates: [2.2, 1.1] 155 | }, 156 | { 157 | coordinates: [2.2, 1.1], 158 | bearing: [45, 32] 159 | }, 160 | { 161 | coordinates: [2.2, 1.1] 162 | } 163 | ], 164 | profile: 'walking', 165 | steps: false 166 | }); 167 | expect(tu.requestConfig(optimization)).toEqual({ 168 | path: '/optimized-trips/v1/mapbox/:profile/:coordinates', 169 | method: 'GET', 170 | params: { 171 | coordinates: '2.2,1.1;2.2,1.1;2.2,1.1;2.2,1.1', 172 | profile: 'walking' 173 | }, 174 | query: { 175 | steps: 'false', 176 | bearings: ';;45,32;', 177 | radiuses: '2000;;;' 178 | } 179 | }); 180 | }); 181 | 182 | test('waypoints.radius can be "unlimited" or a number', () => { 183 | optimization.getOptimization({ 184 | waypoints: [ 185 | { 186 | coordinates: [2.2, 1.1], 187 | radius: 2000 188 | }, 189 | { 190 | coordinates: [2.2, 1.1], 191 | radius: 'unlimited' 192 | } 193 | ] 194 | }); 195 | 196 | expect(tu.requestConfig(optimization)).toEqual({ 197 | path: '/optimized-trips/v1/mapbox/:profile/:coordinates', 198 | method: 'GET', 199 | params: { 200 | coordinates: '2.2,1.1;2.2,1.1', 201 | profile: 'driving' 202 | }, 203 | query: { 204 | radiuses: '2000;unlimited' 205 | } 206 | }); 207 | }); 208 | 209 | test('distributions are formatted into semicolon-separated list of comma-separated number pairs', () => { 210 | optimization.getOptimization({ 211 | waypoints: [ 212 | { 213 | coordinates: [2.2, 1.1] 214 | }, 215 | { 216 | coordinates: [2.2, 1.1] 217 | }, 218 | { 219 | coordinates: [2.2, 1.1] 220 | }, 221 | { 222 | coordinates: [2.2, 1.1] 223 | } 224 | ], 225 | profile: 'driving', 226 | distributions: [ 227 | { 228 | pickup: 0, 229 | dropoff: 1 230 | }, 231 | { 232 | pickup: 2, 233 | dropoff: 3 234 | } 235 | ] 236 | }); 237 | expect(tu.requestConfig(optimization)).toEqual({ 238 | path: '/optimized-trips/v1/mapbox/:profile/:coordinates', 239 | method: 'GET', 240 | params: { 241 | coordinates: '2.2,1.1;2.2,1.1;2.2,1.1;2.2,1.1', 242 | profile: 'driving' 243 | }, 244 | query: { 245 | distributions: '0,1;2,3' 246 | } 247 | }); 248 | }); 249 | }); 250 | -------------------------------------------------------------------------------- /services/__tests__/tilequery.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tilequeryService = require('../tilequery'); 4 | const tu = require('../../test/test-utils'); 5 | 6 | let tilequery; 7 | beforeEach(() => { 8 | tilequery = tilequeryService(tu.mockClient()); 9 | }); 10 | 11 | describe('listFeatures', () => { 12 | test('with minimal config', () => { 13 | tilequery.listFeatures({ 14 | mapIds: ['foo'], 15 | coordinates: [10, 12] 16 | }); 17 | expect(tu.requestConfig(tilequery)).toEqual({ 18 | path: '/v4/:mapIds/tilequery/:coordinates.json', 19 | method: 'GET', 20 | params: { 21 | mapIds: ['foo'], 22 | coordinates: [10, 12] 23 | }, 24 | query: {} 25 | }); 26 | }); 27 | 28 | test('with multiple map IDs', () => { 29 | tilequery.listFeatures({ 30 | mapIds: ['foo', 'bar'], 31 | coordinates: [10, 12] 32 | }); 33 | expect(tu.requestConfig(tilequery)).toEqual({ 34 | path: '/v4/:mapIds/tilequery/:coordinates.json', 35 | method: 'GET', 36 | params: { 37 | mapIds: ['foo', 'bar'], 38 | coordinates: [10, 12] 39 | }, 40 | query: {} 41 | }); 42 | }); 43 | 44 | test('with all config options', () => { 45 | tilequery.listFeatures({ 46 | mapIds: ['foo', 'bar'], 47 | coordinates: [10, 12], 48 | radius: 39, 49 | limit: 3, 50 | dedupe: false, 51 | layers: ['egg', 'sandwich'], 52 | geometry: 'point' 53 | }); 54 | expect(tu.requestConfig(tilequery)).toEqual({ 55 | path: '/v4/:mapIds/tilequery/:coordinates.json', 56 | method: 'GET', 57 | params: { 58 | mapIds: ['foo', 'bar'], 59 | coordinates: [10, 12] 60 | }, 61 | query: { 62 | radius: 39, 63 | limit: 3, 64 | dedupe: false, 65 | layers: ['egg', 'sandwich'], 66 | geometry: 'point' 67 | } 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /services/__tests__/uploads.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const uploadsService = require('../uploads'); 4 | const tu = require('../../test/test-utils'); 5 | 6 | let uploads; 7 | beforeEach(() => { 8 | uploads = uploadsService(tu.mockClient()); 9 | }); 10 | 11 | describe('listUploads', () => { 12 | test('works', () => { 13 | uploads.listUploads(); 14 | expect(tu.requestConfig(uploads)).toEqual({ 15 | path: '/uploads/v1/:ownerId', 16 | method: 'GET', 17 | params: undefined 18 | }); 19 | }); 20 | }); 21 | 22 | describe('createUploadCredentials', () => { 23 | test('works', () => { 24 | uploads.createUploadCredentials({}); 25 | expect(tu.requestConfig(uploads)).toEqual({ 26 | path: '/uploads/v1/:ownerId/credentials', 27 | method: 'POST', 28 | body: undefined 29 | }); 30 | }); 31 | }); 32 | 33 | describe('createUpload', () => { 34 | test('works', () => { 35 | uploads.createUpload({ 36 | tileset: 'username.nameoftileset', 37 | url: 'http://{bucket}.s3.amazonaws.com/{key}', 38 | name: 'dusty_devote' 39 | }); 40 | 41 | expect(tu.requestConfig(uploads)).toEqual({ 42 | path: '/uploads/v1/:ownerId', 43 | method: 'POST', 44 | body: { 45 | tileset: 'username.nameoftileset', 46 | url: 'http://{bucket}.s3.amazonaws.com/{key}', 47 | name: 'dusty_devote' 48 | } 49 | }); 50 | }); 51 | 52 | test('defaults values', () => { 53 | uploads.createUpload({ 54 | tileset: 'username.nameoftileset', 55 | name: 'disty_devote', 56 | url: 'http://{bucket}.s3.amazonaws.com/{key}' 57 | }); 58 | expect(tu.requestConfig(uploads)).toEqual({ 59 | path: '/uploads/v1/:ownerId', 60 | method: 'POST', 61 | body: { 62 | tileset: 'username.nameoftileset', 63 | url: 'http://{bucket}.s3.amazonaws.com/{key}', 64 | name: 'disty_devote' 65 | } 66 | }); 67 | }); 68 | 69 | test('backwards compatibility', () => { 70 | uploads.createUpload({ 71 | mapId: 'tilted_towers', 72 | tilesetName: 'dusty_devote', 73 | url: 'http://{bucket}.s3.amazonaws.com/{key}' 74 | }); 75 | 76 | expect(tu.requestConfig(uploads)).toEqual({ 77 | path: '/uploads/v1/:ownerId', 78 | method: 'POST', 79 | body: { 80 | tileset: 'tilted_towers', 81 | url: 'http://{bucket}.s3.amazonaws.com/{key}', 82 | name: 'dusty_devote' 83 | } 84 | }); 85 | }); 86 | }); 87 | 88 | describe('getUpload', () => { 89 | test('works', () => { 90 | uploads.getUpload({ uploadId: 'ulpod' }); 91 | expect(tu.requestConfig(uploads)).toEqual({ 92 | path: '/uploads/v1/:ownerId/:uploadId', 93 | method: 'GET', 94 | params: { 95 | uploadId: 'ulpod' 96 | } 97 | }); 98 | }); 99 | }); 100 | 101 | describe('removeUpload', () => { 102 | test('works', () => { 103 | uploads.deleteUpload({ 104 | uploadId: 'ulpod' 105 | }); 106 | expect(tu.requestConfig(uploads)).toEqual({ 107 | path: '/uploads/v1/:ownerId/:uploadId', 108 | method: 'DELETE', 109 | params: { 110 | uploadId: 'ulpod' 111 | } 112 | }); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /services/geocoding.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var xtend = require('xtend'); 4 | var v = require('./service-helpers/validator'); 5 | var pick = require('./service-helpers/pick'); 6 | var stringifyBooleans = require('./service-helpers/stringify-booleans'); 7 | var createServiceFactory = require('./service-helpers/create-service-factory'); 8 | 9 | /** 10 | * Geocoding API service. 11 | * 12 | * Learn more about this service and its responses in 13 | * [the HTTP service documentation](https://docs.mapbox.com/api/search/#geocoding). 14 | */ 15 | var Geocoding = {}; 16 | 17 | var featureTypes = [ 18 | 'country', 19 | 'region', 20 | 'postcode', 21 | 'district', 22 | 'place', 23 | 'locality', 24 | 'neighborhood', 25 | 'address', 26 | 'poi', 27 | 'poi.landmark' 28 | ]; 29 | 30 | /** 31 | * Search for a place. 32 | * 33 | * See the [public documentation](https://docs.mapbox.com/api/search/#forward-geocoding). 34 | * 35 | * @param {Object} config 36 | * @param {string} config.query - A place name. 37 | * @param {'mapbox.places'|'mapbox.places-permanent'} [config.mode="mapbox.places"] - Either `mapbox.places` for ephemeral geocoding, or `mapbox.places-permanent` for storing results and batch geocoding. 38 | * @param {Array} [config.countries] - Limits results to the specified countries. 39 | * Each item in the array should be an [ISO 3166 alpha 2 country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2). 40 | * @param {Coordinates|'ip'} [config.proximity] - Bias local results based on a provided coordinate location or a user's IP address. 41 | * @param {Array<'country'|'region'|'postcode'|'district'|'place'|'locality'|'neighborhood'|'address'|'poi'|'poi.landmark'>} [config.types] - Filter results by feature types. 42 | * @param {boolean} [config.autocomplete=true] - Return autocomplete results or not. 43 | * @param {BoundingBox} [config.bbox] - Limit results to a bounding box. 44 | * @param {number} [config.limit=5] - Limit the number of results returned. 45 | * @param {Array} [config.language] - Specify the language to use for response text and, for forward geocoding, query result weighting. 46 | * Options are [IETF language tags](https://en.wikipedia.org/wiki/IETF_language_tag) comprised of a mandatory 47 | * [ISO 639-1 language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) and optionally one or more IETF subtags for country or script. 48 | * @param {boolean} [config.routing=false] - Specify whether to request additional metadata about the recommended navigation destination. Only applicable for address features. 49 | * @param {boolean} [config.fuzzyMatch=true] - Specify whether the Geocoding API should attempt approximate, as well as exact, matching. 50 | * @param {String} [config.worldview="us"] - Filter results to geographic features whose characteristics are defined differently by audiences belonging to various regional, cultural, or political groups. 51 | * @param {String} [config.session_token] - A unique session identifier generated by the client. 52 | * @return {MapiRequest} 53 | * 54 | * @example 55 | * geocodingClient.forwardGeocode({ 56 | * query: 'Paris, France', 57 | * limit: 2 58 | * }) 59 | * .send() 60 | * .then(response => { 61 | * const match = response.body; 62 | * }); 63 | * 64 | * @example 65 | * // geocoding with proximity 66 | * geocodingClient.forwardGeocode({ 67 | * query: 'Paris, France', 68 | * proximity: [-95.4431142, 33.6875431] 69 | * }) 70 | * .send() 71 | * .then(response => { 72 | * const match = response.body; 73 | * }); 74 | * 75 | * // geocoding with countries 76 | * geocodingClient.forwardGeocode({ 77 | * query: 'Paris, France', 78 | * countries: ['fr'] 79 | * }) 80 | * .send() 81 | * .then(response => { 82 | * const match = response.body; 83 | * }); 84 | * 85 | * // geocoding with bounding box 86 | * geocodingClient.forwardGeocode({ 87 | * query: 'Paris, France', 88 | * bbox: [2.14, 48.72, 2.55, 48.96] 89 | * }) 90 | * .send() 91 | * .then(response => { 92 | * const match = response.body; 93 | * }); 94 | */ 95 | Geocoding.forwardGeocode = function(config) { 96 | v.assertShape({ 97 | query: v.required(v.string), 98 | mode: v.oneOf('mapbox.places', 'mapbox.places-permanent'), 99 | countries: v.arrayOf(v.string), 100 | proximity: v.oneOf(v.coordinates, 'ip'), 101 | types: v.arrayOf(v.oneOf(featureTypes)), 102 | autocomplete: v.boolean, 103 | bbox: v.arrayOf(v.number), 104 | limit: v.number, 105 | language: v.arrayOf(v.string), 106 | routing: v.boolean, 107 | fuzzyMatch: v.boolean, 108 | worldview: v.string, 109 | session_token: v.string 110 | })(config); 111 | 112 | config.mode = config.mode || 'mapbox.places'; 113 | 114 | var query = stringifyBooleans( 115 | xtend( 116 | { country: config.countries }, 117 | pick(config, [ 118 | 'proximity', 119 | 'types', 120 | 'autocomplete', 121 | 'bbox', 122 | 'limit', 123 | 'language', 124 | 'routing', 125 | 'fuzzyMatch', 126 | 'worldview', 127 | 'session_token' 128 | ]) 129 | ) 130 | ); 131 | 132 | return this.client.createRequest({ 133 | method: 'GET', 134 | path: '/geocoding/v5/:mode/:query.json', 135 | params: pick(config, ['mode', 'query']), 136 | query: query 137 | }); 138 | }; 139 | 140 | /** 141 | * Search for places near coordinates. 142 | * 143 | * See the [public documentation](https://docs.mapbox.com/api/search/#reverse-geocoding). 144 | * 145 | * @param {Object} config 146 | * @param {Coordinates} config.query - Coordinates at which features will be searched. 147 | * @param {'mapbox.places'|'mapbox.places-permanent'} [config.mode="mapbox.places"] - Either `mapbox.places` for ephemeral geocoding, or `mapbox.places-permanent` for storing results and batch geocoding. 148 | * @param {Array} [config.countries] - Limits results to the specified countries. 149 | * Each item in the array should be an [ISO 3166 alpha 2 country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2). 150 | * @param {Array<'country'|'region'|'postcode'|'district'|'place'|'locality'|'neighborhood'|'address'|'poi'|'poi.landmark'>} [config.types] - Filter results by feature types. 151 | * @param {BoundingBox} [config.bbox] - Limit results to a bounding box. 152 | * @param {number} [config.limit=1] - Limit the number of results returned. If using this option, you must provide a single item for `types`. 153 | * @param {Array} [config.language] - Specify the language to use for response text and, for forward geocoding, query result weighting. 154 | * Options are [IETF language tags](https://en.wikipedia.org/wiki/IETF_language_tag) comprised of a mandatory 155 | * [ISO 639-1 language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) and optionally one or more IETF subtags for country or script. 156 | * @param {'distance'|'score'} [config.reverseMode='distance'] - Set the factors that are used to sort nearby results. 157 | * @param {boolean} [config.routing=false] - Specify whether to request additional metadata about the recommended navigation destination. Only applicable for address features. 158 | * @param {String} [config.worldview="us"] - Filter results to geographic features whose characteristics are defined differently by audiences belonging to various regional, cultural, or political groups. 159 | * @param {String} [config.session_token] - A unique session identifier generated by the client. 160 | * @return {MapiRequest} 161 | * 162 | * @example 163 | * geocodingClient.reverseGeocode({ 164 | * query: [-95.4431142, 33.6875431] 165 | * }) 166 | * .send() 167 | * .then(response => { 168 | * // GeoJSON document with geocoding matches 169 | * const match = response.body; 170 | * }); 171 | */ 172 | Geocoding.reverseGeocode = function(config) { 173 | v.assertShape({ 174 | query: v.required(v.coordinates), 175 | mode: v.oneOf('mapbox.places', 'mapbox.places-permanent'), 176 | countries: v.arrayOf(v.string), 177 | types: v.arrayOf(v.oneOf(featureTypes)), 178 | bbox: v.arrayOf(v.number), 179 | limit: v.number, 180 | language: v.arrayOf(v.string), 181 | reverseMode: v.oneOf('distance', 'score'), 182 | routing: v.boolean, 183 | worldview: v.string, 184 | session_token: v.string 185 | })(config); 186 | 187 | config.mode = config.mode || 'mapbox.places'; 188 | 189 | var query = stringifyBooleans( 190 | xtend( 191 | { country: config.countries }, 192 | pick(config, [ 193 | 'country', 194 | 'types', 195 | 'bbox', 196 | 'limit', 197 | 'language', 198 | 'reverseMode', 199 | 'routing', 200 | 'worldview', 201 | 'session_token' 202 | ]) 203 | ) 204 | ); 205 | 206 | return this.client.createRequest({ 207 | method: 'GET', 208 | path: '/geocoding/v5/:mode/:query.json', 209 | params: pick(config, ['mode', 'query']), 210 | query: query 211 | }); 212 | }; 213 | 214 | module.exports = createServiceFactory(Geocoding); 215 | -------------------------------------------------------------------------------- /services/isochrone.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var v = require('./service-helpers/validator'); 4 | var objectClean = require('./service-helpers/object-clean'); 5 | var stringifyBooleans = require('./service-helpers/stringify-booleans'); 6 | var createServiceFactory = require('./service-helpers/create-service-factory'); 7 | 8 | /** 9 | * Isochrone API service. 10 | * 11 | * Learn more about this service and its responses in 12 | * [the HTTP service documentation](https://docs.mapbox.com/api/navigation/#isochrone). 13 | */ 14 | var Isochrone = {}; 15 | 16 | /** 17 | * Given a location and a routing profile, retrieve up to four isochrone contours 18 | * @param {Object} config 19 | * @param {'driving'|'driving-traffic'|'walking'|'cycling'} [config.profile="driving"] - A Mapbox Directions routing profile ID. 20 | * @param {Coordinates} config.coordinates - A {longitude,latitude} coordinate pair around which to center the isochrone lines. 21 | * @param {Array} [config.minutes] - The times in minutes to use for each isochrone contour. You can specify up to four contours. Times must be in increasing order. The maximum time that can be specified is 60 minutes. Setting minutes and meters in the same time is an error. 22 | * @param {Array} [config.meters] - The distances in meters to use for each isochrone contour. You can specify up to four contours. Distances must be in increasing order. The maximum distance that can be specified is 100000 meters. Setting minutes and meters in the same time is an error. 23 | * @param {Array} [config.colors] - The colors to use for each isochrone contour, specified as hex values without a leading # (for example, ff0000 for red). If this parameter is used, there must be the same number of colors as there are entries in contours_minutes or contours_meters. If no colors are specified, the Isochrone API will assign a default rainbow color scheme to the output. 24 | * @param {boolean} [config.polygons] - Specify whether to return the contours as GeoJSON polygons (true) or linestrings (false, default). When polygons=true, any contour that forms a ring is returned as a polygon. 25 | * @param {number} [config.denoise] - A floating point value from 0.0 to 1.0 that can be used to remove smaller contours. The default is 1.0. A value of 1.0 will only return the largest contour for a given time value. A value of 0.5 drops any contours that are less than half the area of the largest contour in the set of contours for that same time value. 26 | * @param {number} [config.generalize] - A positive floating point value in meters used as the tolerance for Douglas-Peucker generalization. There is no upper bound. If no value is specified in the request, the Isochrone API will choose the most optimized generalization to use for the request. Note that the generalization of contours can lead to self-intersections, as well as intersections of adjacent contours. 27 | 28 | * @return {MapiRequest} 29 | */ 30 | Isochrone.getContours = function(config) { 31 | v.assertShape({ 32 | profile: v.oneOf('driving', 'driving-traffic', 'walking', 'cycling'), 33 | coordinates: v.coordinates, 34 | minutes: v.arrayOf(v.number), 35 | meters: v.arrayOf(v.number), 36 | colors: v.arrayOf(v.string), 37 | polygons: v.boolean, 38 | denoise: v.number, 39 | generalize: v.number, 40 | depart_at: v.string 41 | })(config); 42 | 43 | config.profile = config.profile || 'driving'; 44 | 45 | if (config.minutes !== undefined && config.meters !== undefined) { 46 | throw new Error("minutes and meters can't be specified at the same time"); 47 | } 48 | var contours = config.minutes ? config.minutes : config.meters; 49 | var contours_name = config.minutes ? 'minutes' : 'meters'; 50 | var contoursCount = contours.length; 51 | 52 | if (contoursCount < 1 || contoursCount > 4) { 53 | throw new Error( 54 | contours_name + ' must contain between 1 and 4 contour values' 55 | ); 56 | } 57 | 58 | if ( 59 | config.colors !== undefined && 60 | contours !== undefined && 61 | config.colors.length !== contoursCount 62 | ) { 63 | throw new Error( 64 | 'colors should have the same number of entries as ' + contours_name 65 | ); 66 | } 67 | 68 | if ( 69 | config.minutes !== undefined && 70 | !config.minutes.every(function(minute) { 71 | return minute <= 60; 72 | }) 73 | ) { 74 | throw new Error('minutes must be less than 60'); 75 | } 76 | 77 | var MAX_METERS = 100000; 78 | if ( 79 | config.meters !== undefined && 80 | !config.meters.every(function(meter) { 81 | return meter <= MAX_METERS; 82 | }) 83 | ) { 84 | throw new Error('meters must be less than ' + MAX_METERS); 85 | } 86 | 87 | if (config.generalize && config.generalize < 0) { 88 | throw new Error('generalize tolerance must be a positive number'); 89 | } 90 | 91 | // Strip "#" from colors. 92 | if (config.colors) { 93 | config.colors = config.colors.map(function(color) { 94 | if (color[0] === '#') return color.substring(1); 95 | return color; 96 | }); 97 | } 98 | 99 | var query = stringifyBooleans({ 100 | contours_minutes: config.minutes ? config.minutes.join(',') : null, 101 | contours_meters: config.meters ? config.meters.join(',') : null, 102 | contours_colors: config.colors ? config.colors.join(',') : null, 103 | polygons: config.polygons, 104 | denoise: config.denoise, 105 | generalize: config.generalize, 106 | depart_at: config.depart_at 107 | }); 108 | 109 | return this.client.createRequest({ 110 | method: 'GET', 111 | path: '/isochrone/v1/mapbox/:profile/:coordinates', 112 | params: { 113 | profile: config.profile, 114 | coordinates: config.coordinates.join(',') 115 | }, 116 | query: objectClean(query) 117 | }); 118 | }; 119 | 120 | module.exports = createServiceFactory(Isochrone); 121 | -------------------------------------------------------------------------------- /services/map-matching.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var v = require('./service-helpers/validator'); 4 | var createServiceFactory = require('./service-helpers/create-service-factory'); 5 | var objectClean = require('./service-helpers/object-clean'); 6 | var urlUtils = require('../lib/helpers/url-utils'); 7 | var stringifyBooleans = require('./service-helpers/stringify-booleans'); 8 | 9 | /** 10 | * Map Matching API service. 11 | * 12 | * Learn more about this service and its responses in 13 | * [the HTTP service documentation](https://docs.mapbox.com/api/navigation/#map-matching). 14 | */ 15 | var MapMatching = {}; 16 | 17 | /** 18 | * Snap recorded location traces to roads and paths. 19 | * 20 | * @param {Object} config 21 | * @param {Array} config.points - An ordered array of [`MapMatchingPoint`](#mapmatchingpoint)s, between 2 and 100 (inclusive). 22 | * @param {'driving-traffic'|'driving'|'walking'|'cycling'} [config.profile=driving] - A directions profile ID. 23 | * @param {Array<'duration'|'distance'|'speed'>} [config.annotations] - Specify additional metadata that should be returned. 24 | * @param {'geojson'|'polyline'|'polyline6'} [config.geometries="polyline"] - Format of the returned geometry. 25 | * @param {string} [config.language="en"] - Language of returned turn-by-turn text instructions. 26 | * See [supported languages](https://docs.mapbox.com/api/navigation/#instructions-languages). 27 | * @param {'simplified'|'full'|'false'} [config.overview="simplified"] - Type of returned overview geometry. 28 | * @param {boolean} [config.steps=false] - Whether to return steps and turn-by-turn instructions. 29 | * @param {boolean} [config.tidy=false] - Whether or not to transparently remove clusters and re-sample traces for improved map matching results. 30 | * @return {MapiRequest} 31 | * 32 | * @example 33 | * mapMatchingClient.getMatch({ 34 | * points: [ 35 | * { 36 | * coordinates: [-117.17283, 32.712041], 37 | * approach: 'curb' 38 | * }, 39 | * { 40 | * coordinates: [-117.17291, 32.712256], 41 | * isWaypoint: false 42 | * }, 43 | * { 44 | * coordinates: [-117.17292, 32.712444] 45 | * }, 46 | * { 47 | * coordinates: [-117.172922, 32.71257], 48 | * waypointName: 'point-a', 49 | * approach: 'unrestricted' 50 | * }, 51 | * { 52 | * coordinates: [-117.172985, 32.7126] 53 | * }, 54 | * { 55 | * coordinates: [-117.173143, 32.712597] 56 | * }, 57 | * { 58 | * coordinates: [-117.173345, 32.712546] 59 | * } 60 | * ], 61 | * tidy: false, 62 | * }) 63 | * .send() 64 | * .then(response => { 65 | * const matching = response.body; 66 | * }) 67 | */ 68 | MapMatching.getMatch = function(config) { 69 | v.assertShape({ 70 | points: v.required( 71 | v.arrayOf( 72 | v.shape({ 73 | coordinates: v.required(v.coordinates), 74 | approach: v.oneOf('unrestricted', 'curb'), 75 | radius: v.range([0, 50]), 76 | isWaypoint: v.boolean, 77 | waypointName: v.string, 78 | timestamp: v.date 79 | }) 80 | ) 81 | ), 82 | profile: v.oneOf('driving-traffic', 'driving', 'walking', 'cycling'), 83 | annotations: v.arrayOf(v.oneOf('duration', 'distance', 'speed')), 84 | geometries: v.oneOf('geojson', 'polyline', 'polyline6'), 85 | language: v.string, 86 | overview: v.oneOf('full', 'simplified', 'false'), 87 | steps: v.boolean, 88 | tidy: v.boolean 89 | })(config); 90 | 91 | var pointCount = config.points.length; 92 | if (pointCount < 2 || pointCount > 100) { 93 | throw new Error('points must include between 2 and 100 MapMatchingPoints'); 94 | } 95 | 96 | config.profile = config.profile || 'driving'; 97 | 98 | var path = { 99 | coordinates: [], 100 | approach: [], 101 | radius: [], 102 | isWaypoint: [], 103 | waypointName: [], 104 | timestamp: [] 105 | }; 106 | 107 | /** 108 | * @typedef {Object} MapMatchingPoint 109 | * @property {Coordinates} coordinates 110 | * @property {'unrestricted'|'curb'} [approach="unrestricted"] - Used to indicate how requested routes consider from which side of the road to approach a waypoint. 111 | * @property {number} [radius=5] - A number in meters indicating the assumed precision of the used tracking device. 112 | * @property {boolean} [isWaypoint=true] - Whether this coordinate is waypoint or not. The first and last coordinates will always be waypoints. 113 | * @property {string} [waypointName] - Custom name for the waypoint used for the arrival instruction in banners and voice instructions. Will be ignored unless `isWaypoint` is `true`. 114 | * @property {string | number | Date} [timestamp] - Datetime corresponding to the coordinate. 115 | */ 116 | config.points.forEach(function(obj) { 117 | path.coordinates.push(obj.coordinates[0] + ',' + obj.coordinates[1]); 118 | 119 | // isWaypoint 120 | if (obj.hasOwnProperty('isWaypoint') && obj.isWaypoint != null) { 121 | path.isWaypoint.push(obj.isWaypoint); 122 | } else { 123 | path.isWaypoint.push(true); // default value 124 | } 125 | 126 | if (obj.hasOwnProperty('timestamp') && obj.timestamp != null) { 127 | path.timestamp.push(Number(new Date(obj.timestamp))); 128 | } else { 129 | path.timestamp.push(''); 130 | } 131 | 132 | ['approach', 'radius', 'waypointName'].forEach(function(prop) { 133 | if (obj.hasOwnProperty(prop) && obj[prop] != null) { 134 | path[prop].push(obj[prop]); 135 | } else { 136 | path[prop].push(''); 137 | } 138 | }); 139 | }); 140 | 141 | ['coordinates', 'approach', 'radius', 'waypointName', 'timestamp'].forEach( 142 | function(prop) { 143 | // avoid sending params which are all `;` 144 | if ( 145 | path[prop].every(function(value) { 146 | return value === ''; 147 | }) 148 | ) { 149 | delete path[prop]; 150 | } else { 151 | path[prop] = path[prop].join(';'); 152 | } 153 | } 154 | ); 155 | 156 | // the api requires the first and last items to be true. 157 | path.isWaypoint[0] = true; 158 | path.isWaypoint[path.isWaypoint.length - 1] = true; 159 | 160 | if ( 161 | path.isWaypoint.every(function(value) { 162 | return value === true; 163 | }) 164 | ) { 165 | delete path.isWaypoint; 166 | } else { 167 | // the api requires the indexes to be sent 168 | path.isWaypoint = path.isWaypoint 169 | .map(function(val, i) { 170 | return val === true ? i : ''; 171 | }) 172 | .filter(function(x) { 173 | return x === 0 || Boolean(x); 174 | }) 175 | .join(';'); 176 | } 177 | 178 | var body = stringifyBooleans( 179 | objectClean({ 180 | annotations: config.annotations, 181 | geometries: config.geometries, 182 | language: config.language, 183 | overview: config.overview, 184 | steps: config.steps, 185 | tidy: config.tidy, 186 | approaches: path.approach, 187 | radiuses: path.radius, 188 | waypoints: path.isWaypoint, 189 | timestamps: path.timestamp, 190 | waypoint_names: path.waypointName, 191 | coordinates: path.coordinates 192 | }) 193 | ); 194 | 195 | // the matching api expects a form-urlencoded 196 | // post request. 197 | return this.client.createRequest({ 198 | method: 'POST', 199 | path: '/matching/v5/mapbox/:profile', 200 | params: { 201 | profile: config.profile 202 | }, 203 | body: urlUtils.appendQueryObject('', body).substring(1), // need to remove the char`?` 204 | headers: { 205 | 'content-type': 'application/x-www-form-urlencoded' 206 | } 207 | }); 208 | }; 209 | 210 | module.exports = createServiceFactory(MapMatching); 211 | -------------------------------------------------------------------------------- /services/matrix.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var v = require('./service-helpers/validator'); 4 | var createServiceFactory = require('./service-helpers/create-service-factory'); 5 | var objectClean = require('./service-helpers/object-clean'); 6 | 7 | /** 8 | * Map Matching API service. 9 | * 10 | * Learn more about this service and its responses in 11 | * [the HTTP service documentation](https://docs.mapbox.com/api/navigation/#matrix). 12 | */ 13 | var Matrix = {}; 14 | 15 | /** 16 | * Get a duration and/or distance matrix showing travel times and distances between coordinates. 17 | * 18 | * @param {Object} config 19 | * @param {Array} config.points - An ordered array of [`MatrixPoint`](#matrixpoint)s, between 2 and 100 (inclusive). 20 | * @param {'driving-traffic'|'driving'|'walking'|'cycling'} [config.profile=driving] - A Mapbox Directions routing profile ID. 21 | * @param {'all'|Array} [config.sources] - Use coordinates with given index as sources. 22 | * @param {'all'|Array} [config.destinations] - Use coordinates with given index as destinations. 23 | * @param {Array<'distance'|'duration'>} [config.annotations] - Used to specify resulting matrices. 24 | * @return {MapiRequest} 25 | * 26 | * @example 27 | * matrixClient.getMatrix({ 28 | * points: [ 29 | * { 30 | * coordinates: [2.2, 1.1] 31 | * }, 32 | * { 33 | * coordinates: [2.2, 1.1], 34 | * approach: 'curb' 35 | * }, 36 | * { 37 | * coordinates: [3.2, 1.1] 38 | * }, 39 | * { 40 | * coordinates: [4.2, 1.1] 41 | * } 42 | * ], 43 | * profile: 'walking' 44 | * }) 45 | * .send() 46 | * .then(response => { 47 | * const matrix = response.body; 48 | * }); 49 | */ 50 | Matrix.getMatrix = function(config) { 51 | v.assertShape({ 52 | points: v.required( 53 | v.arrayOf( 54 | v.shape({ 55 | coordinates: v.required(v.coordinates), 56 | approach: v.oneOf('unrestricted', 'curb') 57 | }) 58 | ) 59 | ), 60 | profile: v.oneOf('driving-traffic', 'driving', 'walking', 'cycling'), 61 | annotations: v.arrayOf(v.oneOf('duration', 'distance')), 62 | sources: v.oneOfType(v.equal('all'), v.arrayOf(v.number)), 63 | destinations: v.oneOfType(v.equal('all'), v.arrayOf(v.number)) 64 | })(config); 65 | 66 | var pointCount = config.points.length; 67 | if (pointCount < 2 || pointCount > 100) { 68 | throw new Error('points must include between 2 and 100 MatrixPoints'); 69 | } 70 | 71 | config.profile = config.profile || 'driving'; 72 | 73 | var path = { 74 | coordinates: [], 75 | approach: [] 76 | }; 77 | /** 78 | * @typedef {Object} MatrixPoint 79 | * @property {Coordinates} coordinates - `[longitude, latitude]` 80 | * @property {'unrestricted'|'curb'} [approach="unrestricted"] - Used to indicate how requested routes consider from which side of the road to approach the point. 81 | */ 82 | config.points.forEach(function(obj) { 83 | path.coordinates.push(obj.coordinates[0] + ',' + obj.coordinates[1]); 84 | 85 | if (obj.hasOwnProperty('approach') && obj.approach != null) { 86 | path.approach.push(obj.approach); 87 | } else { 88 | path.approach.push(''); // default value 89 | } 90 | }); 91 | 92 | if ( 93 | path.approach.every(function(value) { 94 | return value === ''; 95 | }) 96 | ) { 97 | delete path.approach; 98 | } else { 99 | path.approach = path.approach.join(';'); 100 | } 101 | 102 | var query = { 103 | sources: Array.isArray(config.sources) 104 | ? config.sources.join(';') 105 | : config.sources, 106 | destinations: Array.isArray(config.destinations) 107 | ? config.destinations.join(';') 108 | : config.destinations, 109 | approaches: path.approach, 110 | annotations: config.annotations && config.annotations.join(',') 111 | }; 112 | 113 | return this.client.createRequest({ 114 | method: 'GET', 115 | path: '/directions-matrix/v1/mapbox/:profile/:coordinates', 116 | params: { 117 | profile: config.profile, 118 | coordinates: path.coordinates.join(';') 119 | }, 120 | query: objectClean(query) 121 | }); 122 | }; 123 | 124 | module.exports = createServiceFactory(Matrix); 125 | -------------------------------------------------------------------------------- /services/optimization.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var v = require('./service-helpers/validator'); 4 | var createServiceFactory = require('./service-helpers/create-service-factory'); 5 | var objectClean = require('./service-helpers/object-clean'); 6 | var stringifyBooleans = require('./service-helpers/stringify-booleans'); 7 | 8 | /** 9 | * Optimization API service. 10 | * 11 | * Learn more about this service and its responses in 12 | * [the HTTP service documentation](https://docs.mapbox.com/api/navigation/#optimization). 13 | */ 14 | var Optimization = {}; 15 | 16 | /** 17 | * Get a duration-optimized route. 18 | * 19 | * Please read [the full HTTP service documentation](https://docs.mapbox.com/api/navigation/#optimization) 20 | * to understand all of the available options. 21 | * 22 | * @param {Object} config 23 | * @param {'driving'|'driving-traffic'|'walking'|'cycling'} [config.profile="driving"] 24 | * @param {Array} config.waypoints - An ordered array of [`OptimizationWaypoint`](#optimizationwaypoint) objects, with at least 2 25 | * @param {Array<'duration'|'distance'|'speed'>} [config.annotations] - Specify additional metadata that should be returned. 26 | * @param {'any'|'last'} [config.destination="any"] - Returned route ends at `any` or `last` coordinate. 27 | * @param {Array} [config.distributions] - An ordered array of [`Distribution`](#distribution) objects, each of which includes a `pickup` and `dropoff` property. `pickup` and `dropoff` properties correspond to an index in the OptimizationWaypoint array. 28 | * @param {'geojson'|'polyline'|'polyline6'} [config.geometries="polyline"] - Format of the returned geometries. 29 | * @param {string} [config.language="en"] - Language of returned turn-by-turn text instructions. 30 | * See options listed in [the HTTP service documentation](https://docs.mapbox.com/api/navigation/#instructions-languages). 31 | * @param {'simplified'|'full'|'false'} [config.overview="simplified"] - Type of returned overview geometry. 32 | * @param {boolean} [config.roundtrip=true] - Specifies whether the trip should complete by returning to the first location. 33 | * @param {'any'|'first'} [config.source="any"] - To begin the route, start either from the first coordinate or let the Optimization API choose. 34 | * @param {boolean} [config.steps=false] - Whether to return steps and turn-by-turn instructions. 35 | * @return {MapiRequest} 36 | */ 37 | Optimization.getOptimization = function(config) { 38 | v.assertShape({ 39 | profile: v.oneOf('driving', 'driving-traffic', 'walking', 'cycling'), 40 | waypoints: v.required( 41 | v.arrayOf( 42 | v.shape({ 43 | coordinates: v.required(v.coordinates), 44 | approach: v.oneOf('unrestricted', 'curb'), 45 | bearing: v.arrayOf(v.range([0, 360])), 46 | radius: v.oneOfType(v.number, v.equal('unlimited')) 47 | }) 48 | ) 49 | ), 50 | annotations: v.arrayOf(v.oneOf('duration', 'distance', 'speed')), 51 | geometries: v.oneOf('geojson', 'polyline', 'polyline6'), 52 | language: v.string, 53 | overview: v.oneOf('simplified', 'full', 'false'), 54 | roundtrip: v.boolean, 55 | steps: v.boolean, 56 | source: v.oneOf('any', 'first'), 57 | destination: v.oneOf('any', 'last'), 58 | distributions: v.arrayOf( 59 | v.shape({ 60 | pickup: v.number, 61 | dropoff: v.number 62 | }) 63 | ) 64 | })(config); 65 | 66 | var path = { 67 | coordinates: [], 68 | approach: [], 69 | bearing: [], 70 | radius: [], 71 | distributions: [] 72 | }; 73 | 74 | var waypointCount = config.waypoints.length; 75 | if (waypointCount < 2) { 76 | throw new Error( 77 | 'waypoints must include at least 2 OptimizationWaypoints' 78 | ); 79 | } 80 | 81 | /** 82 | * @typedef {Object} OptimizationWaypoint 83 | * @property {Coordinates} coordinates 84 | * @property {'unrestricted'|'curb'} [approach="unrestricted"] - Used to indicate how requested routes consider from which side of the road to approach the waypoint. 85 | * @property {[number, number]} [bearing] - Used to filter the road segment the waypoint will be placed on by direction and dictates the angle of approach. 86 | * This option should always be used in conjunction with a `radius`. The first value is an angle clockwise from true north between 0 and 360, 87 | * and the second is the range of degrees the angle can deviate by. 88 | * @property {number|'unlimited'} [radius] - Maximum distance in meters that the coordinate is allowed to move when snapped to a nearby road segment. 89 | */ 90 | config.waypoints.forEach(function(waypoint) { 91 | path.coordinates.push( 92 | waypoint.coordinates[0] + ',' + waypoint.coordinates[1] 93 | ); 94 | 95 | // join props which come in pairs 96 | ['bearing'].forEach(function(prop) { 97 | if (waypoint.hasOwnProperty(prop) && waypoint[prop] != null) { 98 | waypoint[prop] = waypoint[prop].join(','); 99 | } 100 | }); 101 | 102 | ['approach', 'bearing', 'radius'].forEach(function(prop) { 103 | if (waypoint.hasOwnProperty(prop) && waypoint[prop] != null) { 104 | path[prop].push(waypoint[prop]); 105 | } else { 106 | path[prop].push(''); 107 | } 108 | }); 109 | }); 110 | 111 | /** 112 | * @typedef {Object} Distribution 113 | * @property {number} pickup - Array index of the item containing coordinates for the pick-up location in the OptimizationWaypoint array. 114 | * @property {number} dropoff - Array index of the item containing coordinates for the drop-off location in the OptimizationWaypoint array. 115 | */ 116 | // distributions aren't a property of OptimizationWaypoint, so join them separately 117 | if (config.distributions) { 118 | config.distributions.forEach(function(dist) { 119 | path.distributions.push(dist.pickup + ',' + dist.dropoff); 120 | }); 121 | } 122 | 123 | ['approach', 'bearing', 'radius', 'distributions'].forEach(function(prop) { 124 | // avoid sending params which are all `;` 125 | if ( 126 | path[prop].every(function(char) { 127 | return char === ''; 128 | }) 129 | ) { 130 | delete path[prop]; 131 | } else { 132 | path[prop] = path[prop].join(';'); 133 | } 134 | }); 135 | 136 | var query = stringifyBooleans({ 137 | geometries: config.geometries, 138 | language: config.language, 139 | overview: config.overview, 140 | roundtrip: config.roundtrip, 141 | steps: config.steps, 142 | source: config.source, 143 | destination: config.destination, 144 | distributions: path.distributions, 145 | approaches: path.approach, 146 | bearings: path.bearing, 147 | radiuses: path.radius 148 | }); 149 | 150 | return this.client.createRequest({ 151 | method: 'GET', 152 | path: '/optimized-trips/v1/mapbox/:profile/:coordinates', 153 | params: { 154 | profile: config.profile || 'driving', 155 | coordinates: path.coordinates.join(';') 156 | }, 157 | query: objectClean(query) 158 | }); 159 | }; 160 | 161 | module.exports = createServiceFactory(Optimization); 162 | -------------------------------------------------------------------------------- /services/service-helpers/__tests__/fixtures/foo.txt: -------------------------------------------------------------------------------- 1 | foo -------------------------------------------------------------------------------- /services/service-helpers/__tests__/validator-browser.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 'use strict'; 5 | 6 | const v = require('../validator'); 7 | 8 | var t = function(rootcheck) { 9 | return function(value) { 10 | var messages = v.validate(rootcheck, value); 11 | return messages; 12 | }; 13 | }; 14 | 15 | var req = v.required; 16 | 17 | describe('v.file in the browser', () => { 18 | const check = t(v.shape({ prop: req(v.file) })); 19 | 20 | test('rejects strings', () => { 21 | expect(check({ prop: 'path/to/file.txt' })).toEqual([ 22 | 'prop', 23 | 'Blob or ArrayBuffer' 24 | ]); 25 | }); 26 | 27 | test('rejects numbers', () => { 28 | expect(check({ prop: 4 })).toEqual(['prop', 'Blob or ArrayBuffer']); 29 | }); 30 | test('rejects booleans', () => { 31 | expect(check({ prop: false })).toEqual(['prop', 'Blob or ArrayBuffer']); 32 | }); 33 | test('rejects objects', () => { 34 | expect(check({ prop: {} })).toEqual(['prop', 'Blob or ArrayBuffer']); 35 | }); 36 | test('rejects arrays', () => { 37 | expect(check({ prop: [] })).toEqual(['prop', 'Blob or ArrayBuffer']); 38 | }); 39 | 40 | test('accepts Blobs', () => { 41 | expect(check({ prop: new global.Blob(['blobbbbb']) })).toBeUndefined(); 42 | }); 43 | test('accepts ArrayBuffers', () => { 44 | expect(check({ prop: new global.ArrayBuffer(3) })).toBeUndefined(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /services/service-helpers/__tests__/validator.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const v = require('../validator'); 4 | 5 | var t = function(rootcheck) { 6 | return function(value) { 7 | var messages = v.validate(rootcheck, value); 8 | return messages; 9 | }; 10 | }; 11 | 12 | describe('v.file in Node', () => { 13 | var check = t(v.shape({ prop: v.file })); 14 | 15 | test('rejects numbers', () => { 16 | expect(check({ prop: 4 })).toEqual(['prop', 'Filename or Readable stream']); 17 | }); 18 | 19 | test('rejects booleans', () => { 20 | expect(check({ prop: false })).toEqual([ 21 | 'prop', 22 | 'Filename or Readable stream' 23 | ]); 24 | }); 25 | 26 | test('rejects object', () => { 27 | expect(check({ prop: { foo: 'bar' } })).toEqual([ 28 | 'prop', 29 | 'Filename or Readable stream' 30 | ]); 31 | }); 32 | 33 | test('rejects arrays', () => { 34 | expect(check({ prop: ['a', 'b'] })).toEqual([ 35 | 'prop', 36 | 'Filename or Readable stream' 37 | ]); 38 | }); 39 | 40 | test('accepts strings', () => { 41 | expect(check({ prop: 'path/to/file.txt' })).toBeUndefined(); 42 | }); 43 | 44 | test('accepts Readable streams', () => { 45 | expect( 46 | check({ 47 | prop: require('fs').createReadStream( 48 | require('path').join(__dirname, './fixtures/foo.txt') 49 | ) 50 | }) 51 | ).toBeUndefined(); 52 | }); 53 | }); 54 | 55 | describe('v.date', () => { 56 | var check = t(v.date); 57 | 58 | test('rejects values that cannot be passed to the Date constructor to create a valid date', () => { 59 | expect(check(true)).toEqual(['date']); 60 | expect(check('egg sandwich')).toEqual(['date']); 61 | expect(check({ one: 1, two: 2 })).toEqual(['date']); 62 | expect(check(() => {})).toEqual(['date']); 63 | // Make the Date constructor error. 64 | jest.spyOn(global, 'Date').mockImplementationOnce(() => { 65 | throw new Error(); 66 | }); 67 | expect(check(1534285808537)).toEqual(['date']); 68 | }); 69 | 70 | test('accepts values that can be passed to the Date constructor to create a valid date', () => { 71 | expect(check(new Date())).toBeUndefined(); 72 | expect(check('2018-03-03')).toBeUndefined(); 73 | expect(check('Tue Aug 14 2018 15:29:53 GMT-0700 (MST)')).toBeUndefined(); 74 | expect(check(1534285808537)).toBeUndefined(); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /services/service-helpers/create-service-factory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var MapiClient = require('../../lib/classes/mapi-client'); 4 | // This will create the environment-appropriate client. 5 | var createClient = require('../../lib/client'); 6 | 7 | function createServiceFactory(ServicePrototype) { 8 | return function(clientOrConfig) { 9 | var client; 10 | if (MapiClient.prototype.isPrototypeOf(clientOrConfig)) { 11 | client = clientOrConfig; 12 | } else { 13 | client = createClient(clientOrConfig); 14 | } 15 | var service = Object.create(ServicePrototype); 16 | service.client = client; 17 | return service; 18 | }; 19 | } 20 | 21 | module.exports = createServiceFactory; 22 | -------------------------------------------------------------------------------- /services/service-helpers/generic-types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `[longitude, latitude]` 3 | * 4 | * @typedef {Array} Coordinates 5 | */ 6 | 7 | /** 8 | * `[minLongitude, minLatitude, maxLongitude, maxLatitude]` 9 | * 10 | * @typedef {Array} BoundingBox 11 | */ 12 | 13 | /** 14 | * In Node, files must be `ReadableStream`s or paths pointing for the file in the filesystem. 15 | * 16 | * In the browser, files must be `Blob`s or `ArrayBuffer`s. 17 | * 18 | * @typedef {Blob|ArrayBuffer|string|ReadableStream} UploadableFile 19 | */ 20 | -------------------------------------------------------------------------------- /services/service-helpers/object-clean.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var pick = require('./pick'); 4 | 5 | function objectClean(obj) { 6 | return pick(obj, function(_, val) { 7 | return val != null; 8 | }); 9 | } 10 | 11 | module.exports = objectClean; 12 | -------------------------------------------------------------------------------- /services/service-helpers/object-map.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function objectMap(obj, cb) { 4 | return Object.keys(obj).reduce(function(result, key) { 5 | result[key] = cb(key, obj[key]); 6 | return result; 7 | }, {}); 8 | } 9 | 10 | module.exports = objectMap; 11 | -------------------------------------------------------------------------------- /services/service-helpers/pick.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Create a new object by picking properties off an existing object. 5 | * The second param can be overloaded as a callback for 6 | * more fine grained picking of properties. 7 | * @param {Object} source 8 | * @param {Array|function(string, Object):boolean} keys 9 | * @returns {Object} 10 | */ 11 | function pick(source, keys) { 12 | var filter = function(key, val) { 13 | return keys.indexOf(key) !== -1 && val !== undefined; 14 | }; 15 | 16 | if (typeof keys === 'function') { 17 | filter = keys; 18 | } 19 | 20 | return Object.keys(source) 21 | .filter(function(key) { 22 | return filter(key, source[key]); 23 | }) 24 | .reduce(function(result, key) { 25 | result[key] = source[key]; 26 | return result; 27 | }, {}); 28 | } 29 | 30 | module.exports = pick; 31 | -------------------------------------------------------------------------------- /services/service-helpers/stringify-booleans.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var objectMap = require('./object-map'); 4 | 5 | /** 6 | * Stringify all the boolean values in an object, so true becomes "true". 7 | * 8 | * @param {Object} obj 9 | * @returns {Object} 10 | */ 11 | function stringifyBoolean(obj) { 12 | return objectMap(obj, function(_, value) { 13 | return typeof value === 'boolean' ? JSON.stringify(value) : value; 14 | }); 15 | } 16 | 17 | module.exports = stringifyBoolean; 18 | -------------------------------------------------------------------------------- /services/service-helpers/validator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var xtend = require('xtend'); 4 | var v = require('@mapbox/fusspot'); 5 | 6 | function file(value) { 7 | // If we're in a browser so Blob is available, the file must be that. 8 | // In Node, however, it could be a filepath or a pipeable (Readable) stream. 9 | if (typeof window !== 'undefined') { 10 | if (value instanceof global.Blob || value instanceof global.ArrayBuffer) { 11 | return; 12 | } 13 | return 'Blob or ArrayBuffer'; 14 | } 15 | if (typeof value === 'string' || value.pipe !== undefined) { 16 | return; 17 | } 18 | return 'Filename or Readable stream'; 19 | } 20 | 21 | function assertShape(validatorObj, apiName) { 22 | return v.assert(v.strictShape(validatorObj), apiName); 23 | } 24 | 25 | function date(value) { 26 | var msg = 'date'; 27 | if (typeof value === 'boolean') { 28 | return msg; 29 | } 30 | try { 31 | var date = new Date(value); 32 | if (date.getTime && isNaN(date.getTime())) { 33 | return msg; 34 | } 35 | } catch (e) { 36 | return msg; 37 | } 38 | } 39 | 40 | function coordinates(value) { 41 | return v.tuple(v.number, v.number)(value); 42 | } 43 | 44 | module.exports = xtend(v, { 45 | file: file, 46 | date: date, 47 | coordinates: coordinates, 48 | assertShape: assertShape 49 | }); 50 | -------------------------------------------------------------------------------- /services/tilequery.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var v = require('./service-helpers/validator'); 4 | var pick = require('./service-helpers/pick'); 5 | var createServiceFactory = require('./service-helpers/create-service-factory'); 6 | 7 | /** 8 | * Tilequery API service. 9 | * 10 | * Learn more about this service and its responses in 11 | * [the HTTP service documentation](https://docs.mapbox.com/api/maps/#tilequery). 12 | */ 13 | var Tilequery = {}; 14 | 15 | /** 16 | * List features within a radius of a point on a map (or several maps). 17 | * 18 | * @param {Object} config 19 | * @param {Array} config.mapIds - The maps being queried. 20 | * If you need to composite multiple layers, provide multiple map IDs. 21 | * @param {Coordinates} config.coordinates - The longitude and latitude to be queried. 22 | * @param {number} [config.radius=0] - The approximate distance in meters to query for features. 23 | * @param {number} [config.limit=5] - The number of features to return, between 1 and 50. 24 | * @param {boolean} [config.dedupe=true] - Whether or not to deduplicate results. 25 | * @param {'polygon'|'linestring'|'point'} [config.geometry] - Queries for a specific geometry type. 26 | * @param {Array} [config.layers] - IDs of vector layers to query. 27 | * @return {MapiRequest} 28 | * 29 | * @example 30 | * tilequeryClient.listFeatures({ 31 | * mapIds: ['mapbox.mapbox-streets-v8'], 32 | * coordinates: [-122.42901, 37.80633], 33 | * radius: 10 34 | * }) 35 | * .send() 36 | * .then(response => { 37 | * const features = response.body; 38 | * }); 39 | */ 40 | Tilequery.listFeatures = function(config) { 41 | v.assertShape({ 42 | mapIds: v.required(v.arrayOf(v.string)), 43 | coordinates: v.required(v.coordinates), 44 | radius: v.number, 45 | limit: v.range([1, 50]), 46 | dedupe: v.boolean, 47 | geometry: v.oneOf('polygon', 'linestring', 'point'), 48 | layers: v.arrayOf(v.string) 49 | })(config); 50 | 51 | return this.client.createRequest({ 52 | method: 'GET', 53 | path: '/v4/:mapIds/tilequery/:coordinates.json', 54 | params: { 55 | mapIds: config.mapIds, 56 | coordinates: config.coordinates 57 | }, 58 | query: pick(config, ['radius', 'limit', 'dedupe', 'layers', 'geometry']) 59 | }); 60 | }; 61 | 62 | module.exports = createServiceFactory(Tilequery); 63 | -------------------------------------------------------------------------------- /services/tokens.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var v = require('./service-helpers/validator'); 4 | var pick = require('./service-helpers/pick'); 5 | var createServiceFactory = require('./service-helpers/create-service-factory'); 6 | 7 | /** 8 | * Tokens API service. 9 | * 10 | * Learn more about this service and its responses in 11 | * [the HTTP service documentation](https://docs.mapbox.com/api/accounts/#tokens). 12 | */ 13 | var Tokens = {}; 14 | 15 | /** 16 | * List your access tokens. 17 | * 18 | * See the [corresponding HTTP service documentation](https://docs.mapbox.com/api/accounts/#list-tokens). 19 | * 20 | * @return {MapiRequest} 21 | * 22 | * @example 23 | * tokensClient.listTokens() 24 | * .send() 25 | * .then(response => { 26 | * const tokens = response.body; 27 | * }); 28 | */ 29 | Tokens.listTokens = function() { 30 | return this.client.createRequest({ 31 | method: 'GET', 32 | path: '/tokens/v2/:ownerId' 33 | }); 34 | }; 35 | 36 | /** 37 | * Create a new access token. 38 | * 39 | * See the [corresponding HTTP service documentation](https://docs.mapbox.com/api/accounts/#create-a-token). 40 | * 41 | * @param {Object} [config] 42 | * @param {string} [config.note] 43 | * @param {Array} [config.scopes] 44 | * @param {Array} [config.resources] 45 | * @param {Array} [config.allowedUrls] 46 | * @param {Array<{ platform: string, bundleId: string }>} [config.allowedApplications] This option restricts tokens with an Application Bundle ID. The feature is in beta and is only available to our selected customers. For more information, please contact sales. 47 | * @return {MapiRequest} 48 | * 49 | * @example 50 | * tokensClient.createToken({ 51 | * note: 'datasets-token', 52 | * scopes: ['datasets:write', 'datasets:read'] 53 | * }) 54 | * .send() 55 | * .then(response => { 56 | * const token = response.body; 57 | * }); 58 | */ 59 | Tokens.createToken = function(config) { 60 | config = config || {}; 61 | v.assertShape({ 62 | note: v.string, 63 | scopes: v.arrayOf(v.string), 64 | resources: v.arrayOf(v.string), 65 | allowedUrls: v.arrayOf(v.string), 66 | allowedApplications: v.arrayOf( 67 | v.shape({ 68 | bundleId: v.string, 69 | platform: v.string 70 | }) 71 | ) 72 | })(config); 73 | 74 | var body = {}; 75 | body.scopes = config.scopes || []; 76 | if (config.note !== undefined) { 77 | body.note = config.note; 78 | } 79 | if (config.resources) { 80 | body.resources = config.resources; 81 | } 82 | if (config.allowedUrls) { 83 | body.allowedUrls = config.allowedUrls; 84 | } 85 | 86 | if (config.allowedApplications) { 87 | body.allowedApplications = config.allowedApplications; 88 | } 89 | 90 | return this.client.createRequest({ 91 | method: 'POST', 92 | path: '/tokens/v2/:ownerId', 93 | params: pick(config, ['ownerId']), 94 | body: body 95 | }); 96 | }; 97 | 98 | /** 99 | * Create a new temporary access token. 100 | * 101 | * See the [corresponding HTTP service documentation](https://docs.mapbox.com/api/accounts/#create-a-temporary-token). 102 | * 103 | * @param {Object} config 104 | * @param {string} config.expires 105 | * @param {Array} config.scopes 106 | * @return {MapiRequest} 107 | * 108 | * @example 109 | * tokensClient.createTemporaryToken({ 110 | * scopes: ['datasets:write', 'datasets:read'] 111 | * }) 112 | * .send() 113 | * .then(response => { 114 | * const token = response.body; 115 | * }); 116 | */ 117 | Tokens.createTemporaryToken = function(config) { 118 | v.assertShape({ 119 | expires: v.required(v.date), 120 | scopes: v.required(v.arrayOf(v.string)) 121 | })(config); 122 | 123 | return this.client.createRequest({ 124 | method: 'POST', 125 | path: '/tokens/v2/:ownerId', 126 | params: pick(config, ['ownerId']), 127 | body: { 128 | expires: new Date(config.expires).toISOString(), 129 | scopes: config.scopes 130 | } 131 | }); 132 | }; 133 | 134 | /** 135 | * Update an access token. 136 | * 137 | * See the [corresponding HTTP service documentation](https://docs.mapbox.com/api/accounts/#update-a-token). 138 | * 139 | * @param {Object} config 140 | * @param {string} config.tokenId 141 | * @param {string} [config.note] 142 | * @param {Array} [config.scopes] 143 | * @param {Array} [config.resources] 144 | * @param {Array | null} [config.allowedUrls] 145 | * @param {Array<{ platform: string, bundleId: string }> | null} [config.allowedApplications] This option restricts tokens with an Application Bundle ID. The feature is in beta and is only available to our selected customers. For more information, please contact sales. 146 | * @return {MapiRequest} 147 | * 148 | * @example 149 | * tokensClient.updateToken({ 150 | * tokenId: 'cijucimbe000brbkt48d0dhcx', 151 | * note: 'datasets-token', 152 | * scopes: ['datasets:write', 'datasets:read'] 153 | * }) 154 | * .send() 155 | * .then(response => { 156 | * const token = response.body; 157 | * }); 158 | */ 159 | Tokens.updateToken = function(config) { 160 | v.assertShape({ 161 | tokenId: v.required(v.string), 162 | note: v.string, 163 | scopes: v.arrayOf(v.string), 164 | resources: v.arrayOf(v.string), 165 | allowedUrls: v.arrayOf(v.string), 166 | allowedApplications: v.arrayOf( 167 | v.shape({ 168 | bundleId: v.string, 169 | platform: v.string 170 | }) 171 | ) 172 | })(config); 173 | 174 | var body = {}; 175 | if (config.scopes) { 176 | body.scopes = config.scopes; 177 | } 178 | if (config.note !== undefined) { 179 | body.note = config.note; 180 | } 181 | if (config.resources || config.resources === null) { 182 | body.resources = config.resources; 183 | } 184 | if (config.allowedUrls || config.allowedUrls === null) { 185 | body.allowedUrls = config.allowedUrls; 186 | } 187 | 188 | if (config.allowedApplications || config.allowedApplications === null) { 189 | body.allowedApplications = config.allowedApplications; 190 | } 191 | 192 | return this.client.createRequest({ 193 | method: 'PATCH', 194 | path: '/tokens/v2/:ownerId/:tokenId', 195 | params: pick(config, ['ownerId', 'tokenId']), 196 | body: body 197 | }); 198 | }; 199 | 200 | /** 201 | * Get data about the client's access token. 202 | * 203 | * See the [corresponding HTTP service documentation](https://docs.mapbox.com/api/accounts/#retrieve-a-token). 204 | * 205 | * @return {MapiRequest} 206 | * 207 | * @example 208 | * tokensClient.getToken() 209 | * .send() 210 | * .then(response => { 211 | * const token = response.body; 212 | * }); 213 | */ 214 | Tokens.getToken = function() { 215 | return this.client.createRequest({ 216 | method: 'GET', 217 | path: '/tokens/v2' 218 | }); 219 | }; 220 | 221 | /** 222 | * Delete an access token. 223 | * 224 | * See the [corresponding HTTP service documentation](https://docs.mapbox.com/api/accounts/#delete-a-token). 225 | * 226 | * @param {Object} config 227 | * @param {string} config.tokenId 228 | * @return {MapiRequest} 229 | * 230 | * @example 231 | * tokensClient.deleteToken({ 232 | * tokenId: 'cijucimbe000brbkt48d0dhcx' 233 | * }) 234 | * .send() 235 | * .then(response => { 236 | * // Token successfully deleted. 237 | * }); 238 | */ 239 | Tokens.deleteToken = function(config) { 240 | v.assertShape({ 241 | tokenId: v.required(v.string) 242 | })(config); 243 | 244 | return this.client.createRequest({ 245 | method: 'DELETE', 246 | path: '/tokens/v2/:ownerId/:tokenId', 247 | params: pick(config, ['ownerId', 'tokenId']) 248 | }); 249 | }; 250 | 251 | /** 252 | * List your available scopes. Each item is a metadata 253 | * object about the scope, not just the string scope. 254 | * 255 | * See the [corresponding HTTP service documentation](https://docs.mapbox.com/api/accounts/#list-scopes). 256 | * 257 | * @return {MapiRequest} 258 | * 259 | * @example 260 | * tokensClient.listScopes() 261 | * .send() 262 | * .then(response => { 263 | * const scopes = response.body; 264 | * }); 265 | */ 266 | Tokens.listScopes = function() { 267 | return this.client.createRequest({ 268 | method: 'GET', 269 | path: '/scopes/v1/:ownerId' 270 | }); 271 | }; 272 | 273 | module.exports = createServiceFactory(Tokens); 274 | -------------------------------------------------------------------------------- /services/uploads.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var v = require('./service-helpers/validator'); 4 | var createServiceFactory = require('./service-helpers/create-service-factory'); 5 | var pick = require('./service-helpers/pick'); 6 | 7 | /** 8 | * Uploads API service. 9 | * 10 | * Learn more about this service and its responses in 11 | * [the HTTP service documentation](https://docs.mapbox.com/api/maps/#uploads). 12 | */ 13 | var Uploads = {}; 14 | 15 | /** 16 | * List the statuses of all recent uploads. 17 | * 18 | * See the [corresponding HTTP service documentation](https://docs.mapbox.com/api/maps/#retrieve-recent-upload-statuses). 19 | * 20 | * @param {Object} [config] 21 | * @param {boolean} [config.reverse] - List uploads in chronological order, rather than reverse chronological order. 22 | * @return {MapiRequest} 23 | * 24 | * @example 25 | * uploadsClient.listUploads() 26 | * .send() 27 | * .then(response => { 28 | * const uploads = response.body; 29 | * }); 30 | */ 31 | Uploads.listUploads = function(config) { 32 | v.assertShape({ 33 | reverse: v.boolean 34 | })(config); 35 | 36 | return this.client.createRequest({ 37 | method: 'GET', 38 | path: '/uploads/v1/:ownerId', 39 | query: config 40 | }); 41 | }; 42 | 43 | /** 44 | * Create S3 credentials. 45 | * 46 | * See the [corresponding HTTP service documentation](https://docs.mapbox.com/api/maps/#retrieve-s3-credentials). 47 | * 48 | * @return {MapiRequest} 49 | * 50 | * @example 51 | * const AWS = require('aws-sdk'); 52 | * const getCredentials = () => { 53 | * return uploadsClient 54 | * .createUploadCredentials() 55 | * .send() 56 | * .then(response => response.body); 57 | * } 58 | * const putFileOnS3 = (credentials) => { 59 | * const s3 = new AWS.S3({ 60 | * accessKeyId: credentials.accessKeyId, 61 | * secretAccessKey: credentials.secretAccessKey, 62 | * sessionToken: credentials.sessionToken, 63 | * region: 'us-east-1' 64 | * }); 65 | * return s3.putObject({ 66 | * Bucket: credentials.bucket, 67 | * Key: credentials.key, 68 | * Body: fs.createReadStream('/path/to/file.mbtiles') 69 | * }).promise(); 70 | * }; 71 | * 72 | * getCredentials().then(putFileOnS3); 73 | */ 74 | Uploads.createUploadCredentials = function() { 75 | return this.client.createRequest({ 76 | method: 'POST', 77 | path: '/uploads/v1/:ownerId/credentials' 78 | }); 79 | }; 80 | 81 | /** 82 | * Create an upload. 83 | * 84 | * See the [corresponding HTTP service documentation](https://docs.mapbox.com/api/maps/#create-an-upload). 85 | * 86 | * @param {Object} config 87 | * @param {string} config.tileset - The tileset ID to create or replace, in the format `username.nameoftileset`. 88 | * Limited to 32 characters (only `-` and `_` special characters allowed; limit does not include username). 89 | * @param {string} config.url - HTTPS URL of the S3 object provided by [`createUploadCredentials`](#createuploadcredentials) 90 | * @param {string} [config.name] - The name of the tileset. Limited to 64 characters. 91 | * @return {MapiRequest} 92 | * 93 | * @example 94 | * // Response from a call to createUploadCredentials 95 | * const credentials = { 96 | * accessKeyId: '{accessKeyId}', 97 | * bucket: '{bucket}', 98 | * key: '{key}', 99 | * secretAccessKey: '{secretAccessKey}', 100 | * sessionToken: '{sessionToken}', 101 | * url: '{s3 url}' 102 | * }; 103 | * uploadsClient.createUpload({ 104 | * tileset: `${myUsername}.${myTileset}`, 105 | * url: credentials.url, 106 | * name: 'my uploads name', 107 | * }) 108 | * .send() 109 | * .then(response => { 110 | * const upload = response.body; 111 | * }); 112 | */ 113 | Uploads.createUpload = function(config) { 114 | v.assertShape({ 115 | url: v.required(v.string), 116 | tileset: v.string, 117 | name: v.string, 118 | mapId: v.string, 119 | tilesetName: v.string 120 | })(config); 121 | 122 | if (!config.tileset && !config.mapId) { 123 | throw new Error('tileset or mapId must be defined'); 124 | } 125 | 126 | if (!config.name && !config.tilesetName) { 127 | throw new Error('name or tilesetName must be defined'); 128 | } 129 | 130 | // Support old mapId option 131 | if (config.mapId) { 132 | config.tileset = config.mapId; 133 | } 134 | 135 | // Support old tilesetName option 136 | if (config.tilesetName) { 137 | config.name = config.tilesetName; 138 | } 139 | 140 | return this.client.createRequest({ 141 | method: 'POST', 142 | path: '/uploads/v1/:ownerId', 143 | body: pick(config, ['tileset', 'url', 'name']) 144 | }); 145 | }; 146 | 147 | /** 148 | * Get an upload's status. 149 | * 150 | * See the [corresponding HTTP service documentation](https://docs.mapbox.com/api/maps/#retrieve-upload-status). 151 | * 152 | * @param {Object} config 153 | * @param {string} config.uploadId 154 | * @return {MapiRequest} 155 | * 156 | * @example 157 | * uploadsClient.getUpload({ 158 | * uploadId: '{upload_id}' 159 | * }) 160 | * .send() 161 | * .then(response => { 162 | * const status = response.body; 163 | * }); 164 | */ 165 | Uploads.getUpload = function(config) { 166 | v.assertShape({ 167 | uploadId: v.required(v.string) 168 | })(config); 169 | 170 | return this.client.createRequest({ 171 | method: 'GET', 172 | path: '/uploads/v1/:ownerId/:uploadId', 173 | params: config 174 | }); 175 | }; 176 | 177 | /** 178 | * Delete an upload. 179 | * 180 | * See the [corresponding HTTP service documentation](https://docs.mapbox.com/api/maps/#remove-an-upload-status). 181 | * 182 | * @param {Object} config 183 | * @param {string} config.uploadId 184 | * @return {MapiRequest} 185 | * 186 | * @example 187 | * uploadsClient.deleteUpload({ 188 | * uploadId: '{upload_id}' 189 | * }) 190 | * .send() 191 | * .then(response => { 192 | * // Upload successfully deleted. 193 | * }); 194 | */ 195 | Uploads.deleteUpload = function(config) { 196 | v.assertShape({ 197 | uploadId: v.required(v.string) 198 | })(config); 199 | 200 | return this.client.createRequest({ 201 | method: 'DELETE', 202 | path: '/uploads/v1/:ownerId/:uploadId', 203 | params: config 204 | }); 205 | }; 206 | 207 | module.exports = createServiceFactory(Uploads); 208 | -------------------------------------------------------------------------------- /test/browser-interface.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom-global 3 | */ 4 | 'use strict'; 5 | const mockXHR = require('xhr-mock').default; 6 | const browserClient = require('../lib/browser/browser-client'); 7 | const constants = require('../lib/constants'); 8 | const tu = require('./test-utils'); 9 | const testSharedInterface = require('./test-shared-interface'); 10 | 11 | describe('shared interface tests', () => { 12 | testSharedInterface(browserClient, true); // second argument sets isBrowserClient to true 13 | }); 14 | 15 | test('errors early if access token not provided', () => { 16 | tu.expectError( 17 | () => browserClient(), 18 | error => { 19 | expect(error.message).toMatch(/access token/); 20 | } 21 | ); 22 | }); 23 | 24 | // Note: there are discrepancies between the node progress events 25 | // and browser progress events. For now we are assuming 26 | // the discrepancies are not worth normalizing across these 27 | // two platforms as no one would be planning to use them 28 | // in unison. 29 | describe('test progress events', () => { 30 | let request; 31 | const { mockToken } = tu; 32 | 33 | afterEach(() => { 34 | mockXHR.teardown(); 35 | }); 36 | 37 | beforeEach(() => { 38 | mockXHR.setup(); 39 | const accessToken = mockToken(); 40 | const responseBody = { mockStyle: true }; 41 | 42 | // To mock the progress event 43 | // server needs to send the `Content-length` header 44 | // ref: https://github.com/jameslnewell/xhr-mock/tree/master/packages/xhr-mock#upload-progress 45 | mockXHR.get( 46 | `https://api.mapbox.com/styles/v1/mockuser/foo?access_token=${accessToken}`, 47 | { 48 | status: 200, 49 | headers: { 50 | 'Content-Length': JSON.stringify(responseBody).length, 51 | 'Content-Type': 'application/json; charset=utf-8' 52 | }, 53 | body: JSON.stringify(responseBody) 54 | } 55 | ); 56 | const client = browserClient({ accessToken }); 57 | 58 | request = client.createRequest({ 59 | method: 'GET', 60 | path: '/styles/v1/:ownerId/:styleId', 61 | params: { styleId: 'foo' } 62 | }); 63 | }); 64 | 65 | test('request.emitter should not emit uploadProgress events', () => { 66 | let progressUpload = []; 67 | request.emitter.on(constants.EVENT_PROGRESS_UPLOAD, resp => { 68 | progressUpload.push(Object.assign({}, resp)); 69 | }); 70 | 71 | return request.send().then(() => { 72 | expect(progressUpload).toEqual([]); 73 | }); 74 | }); 75 | 76 | test('request.emitter should emit downloadProgress events', () => { 77 | let progressDownload = []; 78 | request.emitter.on(constants.EVENT_PROGRESS_DOWNLOAD, resp => { 79 | progressDownload.push(Object.assign({}, resp)); 80 | }); 81 | 82 | return request.send().then(() => { 83 | expect(progressDownload).toEqual([ 84 | { percent: 100, total: 18, transferred: 18 } 85 | ]); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/bundle.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const camelcase = require('camelcase'); 6 | const mapboxSdk = require('../bundle'); 7 | 8 | describe('includes all services', () => { 9 | const services = fs 10 | .readdirSync(path.join(__dirname, '../services')) 11 | .filter(filename => path.extname(filename) === '.js') 12 | .map(filename => camelcase(path.basename(filename, '.js'))); 13 | 14 | // Mock token lifted from parse-mapbox-token tests. 15 | const client = mapboxSdk({ 16 | accessToken: 17 | 'pk.eyJ1IjoiZmFrZXVzZXIiLCJhIjoicHBvb2xsIn0.sbihZCZJ56-fsFNKHXF8YQ' 18 | }); 19 | 20 | services.forEach(service => { 21 | test(`includes ${service}`, () => { 22 | expect(client[service]).toBeTruthy(); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/node-interface.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const nodeClient = require('../lib/node/node-client'); 4 | const constants = require('../lib/constants'); 5 | const tu = require('./test-utils'); 6 | const testSharedInterface = require('./test-shared-interface'); 7 | 8 | describe('shared interface tests', () => { 9 | testSharedInterface(nodeClient); 10 | }); 11 | 12 | test('errors early if access token not provided', () => { 13 | tu.expectError( 14 | () => nodeClient(), 15 | error => { 16 | expect(error.message).toMatch(/access token/); 17 | } 18 | ); 19 | }); 20 | 21 | describe('test node progress events', () => { 22 | let request; 23 | let server; 24 | let createLocalClient; 25 | const { mockToken } = tu; 26 | 27 | beforeAll(() => { 28 | return tu.mockServer().then(s => { 29 | server = s; 30 | createLocalClient = server.localClient(nodeClient); 31 | }); 32 | }); 33 | 34 | afterAll(done => { 35 | server.close(done); 36 | }); 37 | 38 | afterEach(() => { 39 | server.reset(); 40 | }); 41 | 42 | beforeEach(() => { 43 | server.setResponse((req, res) => { 44 | res.append('Content-Type', 'application/json; charset=utf-8'); 45 | res.json({ mockStyle: true }); 46 | }); 47 | 48 | const accessToken = mockToken(); 49 | const client = createLocalClient({ accessToken }); 50 | request = client.createRequest({ 51 | method: 'GET', 52 | path: '/styles/v1/:ownerId/:styleId', 53 | params: { styleId: 'foo' } 54 | }); 55 | }); 56 | 57 | test('request.emitter should emit uploadProgress events', () => { 58 | let progressUpload = []; 59 | request.emitter.on(constants.EVENT_PROGRESS_UPLOAD, resp => { 60 | progressUpload.push(Object.assign({}, resp)); 61 | }); 62 | 63 | return request.send().then(() => { 64 | expect(progressUpload).toEqual([ 65 | { percent: 0, total: undefined, transferred: 0 }, 66 | { percent: 100, total: 0, transferred: 0 } 67 | ]); 68 | }); 69 | }); 70 | 71 | test('request.emitter should emit downloadProgress events', () => { 72 | let progressDownload = []; 73 | request.emitter.on(constants.EVENT_PROGRESS_DOWNLOAD, resp => { 74 | progressDownload.push(Object.assign({}, resp)); 75 | }); 76 | 77 | return request.send().then(() => { 78 | expect(progressDownload).toEqual([ 79 | { percent: 0, total: 18, transferred: 0 }, 80 | { percent: 100, total: 18, transferred: 18 } 81 | ]); 82 | }); 83 | }); 84 | }); 85 | 86 | describe('test empty request bodies', () => { 87 | let server; 88 | let createLocalClient; 89 | const { mockToken } = tu; 90 | 91 | beforeAll(() => { 92 | return tu.mockServer().then(s => { 93 | server = s; 94 | createLocalClient = server.localClient(nodeClient); 95 | }); 96 | }); 97 | 98 | afterAll(done => { 99 | server.close(done); 100 | }); 101 | 102 | afterEach(() => { 103 | server.reset(); 104 | }); 105 | 106 | beforeEach(() => { 107 | server.setResponse((req, res) => { 108 | res.append('Content-Type', 'application/json; charset=utf-8'); 109 | res.json({ mockStyle: true }); 110 | }); 111 | }); 112 | 113 | test('request for POST, PATCH, PUT, or DELETE without an explicit body should include empty string as body', () => { 114 | // https://github.com/sindresorhus/got/issues/2303 115 | const accessToken = mockToken(); 116 | const client = createLocalClient({ accessToken }); 117 | const request = client.createRequest({ 118 | method: 'DELETE', 119 | path: '/tilesets/v1/:tilesetId', 120 | params: { tilesetId: 'foo.bar' } 121 | }); 122 | 123 | return request.send().then(({ statusCode }) => { 124 | expect(statusCode).toBe(200); 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /test/test-utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const bodyParser = require('body-parser'); 5 | const base64url = require('base64-url'); 6 | const getPort = require('get-port'); 7 | const MapiClient = require('../lib/classes/mapi-client'); 8 | 9 | function requestConfig(service) { 10 | return service.client.createRequest.mock.calls[0][0]; 11 | } 12 | 13 | function mockClient() { 14 | var client = { 15 | createRequest: jest.fn(), 16 | abortRequest: jest.fn() 17 | }; 18 | // Allow for Object.isPrototypeOf checks. 19 | Object.setPrototypeOf(client, MapiClient.prototype); 20 | return client; 21 | } 22 | 23 | function mockServer() { 24 | let handleRequest; 25 | let handleResponse; 26 | 27 | const reset = () => { 28 | handleRequest = () => {}; 29 | handleResponse = (req, res) => { 30 | if (req.headers['content-type'] === 'application/octet-stream') { 31 | const json = JSON.stringify({ 32 | test: 'test' 33 | }); 34 | const buf = Buffer.from(json); 35 | res.writeHead(200, { 36 | 'Content-Type': 'application/octet-stream', 37 | 'Content-disposition': 'attachment; filename=data.json' 38 | }); 39 | res.write(buf); 40 | res.end(); 41 | } 42 | 43 | res.send(); 44 | }; 45 | }; 46 | reset(); 47 | 48 | const setResponse = cb => { 49 | handleResponse = cb; 50 | }; 51 | 52 | const captureRequest = sendRequest => { 53 | const promiseRequest = new Promise((resolve, reject) => { 54 | try { 55 | handleRequest = req => resolve(req); 56 | } catch (error) { 57 | reject(error); 58 | } 59 | }); 60 | return sendRequest().then(() => promiseRequest); 61 | }; 62 | 63 | const app = express(); 64 | app.use(bodyParser.json()); 65 | app.use((req, res) => { 66 | // The browser tests require Access-Control-Allow-Origin for CORS, 67 | // so we'll just universally set Access-Control-Expose-Headers here. 68 | res.header('Access-Control-Expose-Headers', [ 69 | 'Access-Control-Allow-Origin', 70 | 'Link' 71 | ]); 72 | 73 | // allow the three odd headers we are testing for in `test-shared-interface` 74 | res.header('Access-Control-Allow-Headers', [ 75 | 'if-unmodified-since', 76 | 'x-horse-name', 77 | 'x-dog-name' 78 | ]); 79 | 80 | res.append('Access-Control-Allow-Origin', '*'); 81 | handleRequest(req); 82 | handleResponse(req, res); 83 | }); 84 | 85 | return getPort().then(port => { 86 | const origin = `http://localhost:${port}`; 87 | const nodeServer = app.listen(port); 88 | 89 | if (global.jsdom) { 90 | global.jsdom.reconfigure({ 91 | url: origin 92 | }); 93 | } 94 | 95 | const localClient = createClient => { 96 | return options => { 97 | return createClient(Object.assign({}, options, { origin })); 98 | }; 99 | }; 100 | 101 | const close = cb => { 102 | nodeServer.close(() => { 103 | cb(); 104 | }); 105 | }; 106 | 107 | return { 108 | captureRequest, 109 | setResponse, 110 | reset, 111 | close, 112 | origin, 113 | port, 114 | localClient 115 | }; 116 | }); 117 | } 118 | 119 | function mockToken(header = 'pk', payload = {}, signature = 'sss') { 120 | const defaultedPayload = Object.assign( 121 | { 122 | u: 'mockuser', 123 | a: 'mockauth' 124 | }, 125 | payload 126 | ); 127 | const encodedPayload = base64url.encode(JSON.stringify(defaultedPayload)); 128 | return [header, encodedPayload, signature].join('.'); 129 | } 130 | 131 | function expectRejection(p, cb) { 132 | return p 133 | .then(() => { 134 | throw new Error('should have rejected'); 135 | }) 136 | .catch(cb); 137 | } 138 | 139 | function expectError(fn, cb) { 140 | try { 141 | fn(); 142 | throw new Error('should have errored'); 143 | } catch (e) { 144 | cb(e); 145 | } 146 | } 147 | 148 | module.exports = { 149 | requestConfig, 150 | mockClient, 151 | mockServer, 152 | mockToken, 153 | expectRejection, 154 | expectError 155 | }; 156 | -------------------------------------------------------------------------------- /test/try-browser/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": false 5 | } 6 | } -------------------------------------------------------------------------------- /test/try-browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Test the SDK in the browser 4 | 21 | 22 | 23 |

Test the SDK in the browser

24 |

25 | Open the console and use the global method tryServiceMethod. 26 | It should log responses and errors to the console. 27 | Here's its signature: 28 |

29 |

30 |

tryServiceMethod(
31 |   service: string,
32 |   method: string,
33 |   config: ?Object,
34 |   accessToken: ?string,
35 |   callback: ?(response) => void,
36 | )
37 |

38 |

39 | You can paste an access token as the last argument or set it as the global variable MAPBOX_ACCESS_TOKEN. 40 |

41 |

42 | Only use public or very temporary access tokens! This is not a secure place to put a secret token. 43 |

44 |

45 | An example: 46 |

47 |
MAPBOX_ACCESS_TOKEN = 'pk....';
48 | tryServiceMethod('styles', 'getStyle', {
49 |   styleId: 'cjgzfhs7k00072rnkgjlcuxsu'
50 | });
51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /test/try-browser/try-browser.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 'use strict'; 3 | 4 | var mbxClient = require('../..'); 5 | var mbxStyles = require('../../services/styles'); 6 | var mbxTilesets = require('../../services/tilesets'); 7 | var mbxTokens = require('../../services/tokens'); 8 | var mbxDatasets = require('../../services/datasets'); 9 | var mbxTilequery = require('../../services/tilequery'); 10 | var mbxDirections = require('../../services/directions'); 11 | var mbxMapMatching = require('../../services/map-matching'); 12 | var mbxMatrix = require('../../services/matrix'); 13 | var mbxUploads = require('../../services/uploads'); 14 | var mbxGeocoding = require('../../services/geocoding'); 15 | var mbxGeocodingV6 = require('../../services/geocoding-v6'); 16 | var mbxStatic = require('../../services/static'); 17 | 18 | window.tryServiceMethod = function( 19 | serviceName, 20 | methodName, 21 | config, 22 | accessToken, 23 | callback 24 | ) { 25 | config = config || {}; 26 | accessToken = accessToken || window.MAPBOX_ACCESS_TOKEN; 27 | 28 | if (!accessToken) { 29 | throw new Error('ERROR: Could not find an access token'); 30 | } 31 | 32 | if (!serviceName || !methodName) { 33 | throw new Error('ERROR: You must provide a service and method'); 34 | } 35 | 36 | var baseClient = mbxClient({ accessToken: accessToken }); 37 | var services = { 38 | datasets: mbxDatasets(baseClient), 39 | directions: mbxDirections(baseClient), 40 | geocoding: mbxGeocoding(baseClient), 41 | geocodingV6: mbxGeocodingV6(baseClient), 42 | mapMatching: mbxMapMatching(baseClient), 43 | matrix: mbxMatrix(baseClient), 44 | static: mbxStatic(baseClient), 45 | styles: mbxStyles(baseClient), 46 | tilequery: mbxTilequery(baseClient), 47 | tilesets: mbxTilesets(baseClient), 48 | tokens: mbxTokens(baseClient), 49 | uploads: mbxUploads(baseClient) 50 | }; 51 | 52 | var service = services[serviceName]; 53 | if (!service) { 54 | throw new Error('Unknown service "' + serviceName + '"'); 55 | } 56 | 57 | var method = service[methodName]; 58 | if (!method) { 59 | throw new Error( 60 | 'Unknown method "' + methodName + '" for service "' + serviceName + '"' 61 | ); 62 | } 63 | 64 | service[methodName](config) 65 | .send() 66 | .then( 67 | function(response) { 68 | console.log(response); 69 | if (callback) callback(response); 70 | }, 71 | function(error) { 72 | console.error(error); 73 | } 74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /test/try-node.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable no-console */ 3 | 'use strict'; 4 | 5 | const meow = require('meow'); 6 | const mbxClient = require('..'); 7 | const mbxDatasets = require('../services/datasets'); 8 | const mbxDirections = require('../services/directions'); 9 | const mbxGeocoding = require('../services/geocoding'); 10 | const mbxGeocodingV6 = require('../services/geocoding-v6'); 11 | const mbxMapMatching = require('../services/map-matching'); 12 | const mbxMatrix = require('../services/matrix'); 13 | const mbxStyles = require('../services/styles'); 14 | const mbxTilequery = require('../services/tilequery'); 15 | const mbxTilesets = require('../services/tilesets'); 16 | const mbxTokens = require('../services/tokens'); 17 | const mbxUploads = require('../services/uploads'); 18 | 19 | const description = 'FOR TESTING ONLY! Try out the mapbox-sdk.'; 20 | const help = ` 21 | Usage 22 | test/try-node.js [options] 23 | 24 | Set your access token with the env variable MAPBOX_ACCESS_TOKEN 25 | or the option --access-token 26 | 27 | Options 28 | --access-token The access token you want to use. 29 | Can also be set as the env variable 30 | MAPBOX_ACCESS_TOKEN. 31 | 32 | Example 33 | test/try-node.js styles createStyle '{ "styleId": "cjgsq4lcv000r2sp56iiah07e" }' 34 | `; 35 | const cli = meow({ 36 | description, 37 | help, 38 | flags: { 39 | accessToken: { type: 'string' } 40 | } 41 | }); 42 | 43 | const accessToken = cli.flags.accessToken || process.env.MAPBOX_ACCESS_TOKEN; 44 | const serviceName = cli.input[0]; 45 | const methodName = cli.input[1]; 46 | const rawConfig = cli.input[2] || '{}'; 47 | 48 | if (!accessToken) { 49 | console.log('ERROR: Could not find an access token'); 50 | cli.showHelp(); 51 | } 52 | 53 | if (!serviceName || !methodName) { 54 | console.log('ERROR: You must provide a service and method'); 55 | cli.showHelp(); 56 | } 57 | 58 | const baseClient = mbxClient({ accessToken }); 59 | const services = { 60 | datasets: mbxDatasets(baseClient), 61 | directions: mbxDirections(baseClient), 62 | geocoding: mbxGeocoding(baseClient), 63 | geocodingV6: mbxGeocodingV6(baseClient), 64 | matching: mbxMapMatching(baseClient), 65 | matrix: mbxMatrix(baseClient), 66 | styles: mbxStyles(baseClient), 67 | tilequery: mbxTilequery(baseClient), 68 | tilesets: mbxTilesets(baseClient), 69 | tokens: mbxTokens(baseClient), 70 | uploads: mbxUploads(baseClient) 71 | }; 72 | 73 | const service = services[serviceName]; 74 | if (!service) { 75 | throw new Error(`Unknown service "${serviceName}"`); 76 | } 77 | 78 | const method = service[methodName]; 79 | if (!method) { 80 | throw new Error( 81 | `Unknown method "${methodName}" for service "${serviceName}"` 82 | ); 83 | } 84 | 85 | const config = JSON.parse(rawConfig); 86 | 87 | service[methodName](config) 88 | .send() 89 | .then( 90 | response => { 91 | console.log(response); 92 | }, 93 | error => { 94 | console.log(error); 95 | } 96 | ); 97 | --------------------------------------------------------------------------------