├── .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 | [](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*).reduce(function(result, link) {
59 | var parsed = parseLink(link);
60 | if (!parsed) return result;
61 | // rel value can be multiple whitespace-separated rels.
62 | var splitRel = parsed.rel.split(/\s+/);
63 | splitRel.forEach(function(rel) {
64 | if (!result[rel]) {
65 | result[rel] = {
66 | url: parsed.url,
67 | params: parsed.params
68 | };
69 | }
70 | });
71 | return result;
72 | }, {});
73 | }
74 |
75 | module.exports = parseLinkHeader;
76 |
--------------------------------------------------------------------------------
/lib/helpers/url-utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Encode each item of an array individually. The comma
4 | // delimiters should not themselves be encoded.
5 | function encodeArray(arrayValue) {
6 | return arrayValue.map(encodeURIComponent).join(',');
7 | }
8 |
9 | function encodeValue(value) {
10 | if (Array.isArray(value)) {
11 | return encodeArray(value);
12 | }
13 | return encodeURIComponent(String(value));
14 | }
15 |
16 | /**
17 | * Append a query parameter to a URL.
18 | *
19 | * @param {string} url
20 | * @param {string} key
21 | * @param {string|number|boolean|Array<*>>} [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 |