├── .babelrc ├── .circleci └── config.yml ├── .gitignore ├── .jsdoc.json ├── .npmignore ├── LICENSE ├── README.md ├── apisauce.d.ts ├── examples └── github.js ├── github ├── CONTRIBUTING.md └── labels.json ├── lib └── apisauce.ts ├── package.json ├── rollup.config.js ├── test ├── _getFreePort.js ├── _server.js ├── async-request-transform.test.js ├── async-response-transform.test.js ├── async.test.js ├── cancellation.test.js ├── config.test.js ├── data.test.js ├── headers.test.js ├── monitor.test.js ├── no-server.test.js ├── params.test.js ├── post-data.test.js ├── request-transform.test.js ├── response-transform.test.js ├── set-base-url.test.js ├── speed.test.js ├── status.test.js ├── timeout.test.js └── verbs.test.js ├── tsconfig.json └── tslint.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "ignore": "test/*", 4 | "env": { 5 | "development": { 6 | "sourceMaps": "inline" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | 6 | defaults: &defaults 7 | docker: 8 | # Choose the version of Node you want here 9 | - image: cimg/node:19.9 10 | working_directory: ~/repo 11 | 12 | version: 2 13 | jobs: 14 | setup: 15 | <<: *defaults 16 | steps: 17 | - checkout 18 | - restore_cache: 19 | name: Restore node modules 20 | keys: 21 | - v1-dependencies-{{ checksum "package.json" }} 22 | # fallback to using the latest cache if no exact match is found 23 | - v1-dependencies- 24 | - run: 25 | name: Install dependencies 26 | command: yarn install 27 | - save_cache: 28 | name: Save node modules 29 | paths: 30 | - node_modules 31 | key: v1-dependencies-{{ checksum "package.json" }} 32 | 33 | rollup_and_tests: 34 | <<: *defaults 35 | steps: 36 | - checkout 37 | - restore_cache: 38 | name: Restore node modules 39 | keys: 40 | - v1-dependencies-{{ checksum "package.json" }} 41 | # fallback to using the latest cache if no exact match is found 42 | - v1-dependencies- 43 | # dist command compiles the rollup file and also runs tests 44 | - run: 45 | name: Run rollup and tests 46 | command: yarn dist 47 | 48 | publish: 49 | <<: *defaults 50 | steps: 51 | - checkout 52 | - run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 53 | - restore_cache: 54 | name: Restore node modules 55 | keys: 56 | - v1-dependencies-{{ checksum "package.json" }} 57 | # fallback to using the latest cache if no exact match is found 58 | - v1-dependencies- 59 | # Run semantic-release after all the above is set. 60 | - run: 61 | name: Publish to NPM 62 | command: yarn ci:publish # this will be added to your package.json scripts 63 | 64 | workflows: 65 | version: 2 66 | test_and_release: 67 | jobs: 68 | - setup 69 | - rollup_and_tests: 70 | requires: 71 | - setup 72 | - publish: 73 | context: infinitered-npm-package 74 | requires: 75 | - rollup_and_tests 76 | filters: 77 | branches: 78 | only: master 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | docs 3 | .nyc_output 4 | coverage 5 | npm-debug.log 6 | yarn.lock 7 | dist 8 | .vscode 9 | lib/apisauce.js 10 | -------------------------------------------------------------------------------- /.jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true 4 | }, 5 | "source": { 6 | "include": "./dist", 7 | "includePattern": ".js$", 8 | "excludePattern": "(node_modules/|docs)" 9 | }, 10 | "plugins": [ 11 | "plugins/markdown" 12 | ], 13 | "opts": { 14 | "template": "node_modules/docdash", 15 | "encoding": "utf8", 16 | "destination": "docs/", 17 | "recurse": true, 18 | "verbose": true 19 | }, 20 | "templates": { 21 | "cleverLinks": false, 22 | "monospaceLinks": false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .gitignore 3 | .jsdoc.json 4 | gulpfile.js 5 | docs/ 6 | lib/ 7 | test/ 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Steve Kellock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apisauce 2 | 3 | ``` 4 | (Ring ring ring) 5 | < Hello? 6 | > Hi, can I speak to JSON API. 7 | < Speaking. 8 | > Hi, it's me JavaScript. Look, we need to talk. 9 | < Now is not a good time... 10 | > Wait, I just wanted to say, sorry. 11 | < ... 12 | ``` 13 | 14 | Talking to APIs doesn't have to be awkward anymore. 15 | 16 | [![npm module](https://badge.fury.io/js/apisauce.svg)](https://www.npmjs.org/package/apisauce) 17 | 18 | # Features 19 | 20 | - low-fat wrapper for the amazing `axios` http client library 21 | - all responses follow the same flow: success and failure alike 22 | - responses have a `problem` property to help guide exception flow 23 | - attach functions that get called each request 24 | - attach functions that change all request or response data 25 | - detects connection issues (on React Native) 26 | 27 | # Installing 28 | 29 | `npm i apisauce --save` or `yarn add apisauce` 30 | 31 | - Depends on `axios`. 32 | - Compatible with ES5. 33 | - Built with TypeScript. 34 | - Supports Node, the browser, and React Native. 35 | 36 | # Quick Start 37 | 38 | ```js 39 | // showLastCommitMessageForThisLibrary.js 40 | import { create } from 'apisauce' 41 | 42 | // define the api 43 | const api = create({ 44 | baseURL: 'https://api.github.com', 45 | headers: { Accept: 'application/vnd.github.v3+json' }, 46 | }) 47 | 48 | // start making calls 49 | api 50 | .get('/repos/skellock/apisauce/commits') 51 | .then(response => response.data[0].commit.message) 52 | .then(console.log) 53 | 54 | // customizing headers per-request 55 | api.post('/users', { name: 'steve' }, { headers: { 'x-gigawatts': '1.21' } }) 56 | ``` 57 | 58 | See the examples folder for more code. 59 | 60 | # Documentation 61 | 62 | ## Create an API 63 | 64 | You create an api by calling `.create()` and passing in a configuration object. 65 | 66 | ```js 67 | const api = create({ baseURL: 'https://api.github.com' }) 68 | ``` 69 | 70 | The only required property is `baseURL` and it should be the starting point for 71 | your API. It can contain a sub-path and a port as well. 72 | 73 | ```js 74 | const api = create({ baseURL: 'https://example.com/api/v3' }) 75 | ``` 76 | 77 | HTTP request headers for all requests can be included as well. 78 | 79 | ```js 80 | const api = create({ 81 | baseURL: '...', 82 | headers: { 83 | 'X-API-KEY': '123', 84 | 'X-MARKS-THE-SPOT': 'yarrrrr', 85 | }, 86 | }) 87 | ``` 88 | 89 | Default timeouts can be applied too: 90 | 91 | ```js 92 | const api = create({ baseURL: '...', timeout: 30000 }) // 30 seconds 93 | ``` 94 | 95 | You can also pass an already created axios instance 96 | 97 | ```js 98 | import axios from 'axios' 99 | import { create } from 'apisauce' 100 | 101 | const customAxiosInstance = axios.create({ baseURL: 'https://example.com/api/v3' }) 102 | 103 | const apisauceInstance = create({ axiosInstance: customAxiosInstance }) 104 | ``` 105 | 106 | ## Calling The API 107 | 108 | With your fresh `api`, you can now call it like this: 109 | 110 | ```js 111 | api.get('/repos/skellock/apisauce/commits') 112 | api.head('/me') 113 | api.delete('/users/69') 114 | api.post('/todos', { note: 'jump around' }, { headers: { 'x-ray': 'machine' } }) 115 | api.patch('/servers/1', { live: false }) 116 | api.put('/servers/1', { live: true }) 117 | api.link('/images/my_dog.jpg', {}, { headers: { Link: '; rel="tag"' } }) 118 | api.unlink('/images/my_dog.jpg', {}, { headers: { Link: '; rel="tag"' } }) 119 | api.any({ method: 'GET', url: '/product', params: { id: 1 } }) 120 | ``` 121 | 122 | `get`, `head`, `delete`, `link` and `unlink` accept 3 parameters: 123 | 124 | - url - the relative path to the API (required) 125 | - params - Object - query string variables (optional) 126 | - axiosConfig - Object - config passed along to the `axios` request (optional) 127 | 128 | `post`, `put`, and `patch` accept 3 different parameters: 129 | 130 | - url - the relative path to the API (required) 131 | - data - Object - the object jumping the wire 132 | - axiosConfig - Object - config passed along to the `axios` request (optional) 133 | 134 | `any` only accept one parameter 135 | 136 | - config - Object - config passed along to the `axios` request, this object same as `axiosConfig` 137 | 138 | ## Responses 139 | 140 | The responses are promise-based, so you'll need to handle things in a 141 | `.then()` function. 142 | 143 | The promised is always resolved with a `response` object. 144 | 145 | Even if there was a problem with the request! This is one of the goals of 146 | this library. It ensures sane calling code without having to handle `.catch` 147 | and have 2 separate flows. 148 | 149 | A response will always have these 2 properties: 150 | 151 | ``` 152 | ok - Boolean - True if the status code is in the 200's; false otherwise. 153 | problem - String - One of 6 different values (see below - problem codes) 154 | ``` 155 | 156 | If the request made it to the server and got a response of any kind, response 157 | will also have these properties: 158 | 159 | ``` 160 | data - Object - this is probably the thing you're after. 161 | status - Number - the HTTP response code 162 | headers - Object - the HTTP response headers 163 | config - Object - the `axios` config object used to make the request 164 | duration - Number - the number of milliseconds it took to run this request 165 | ``` 166 | 167 | Sometimes on different platforms you need access to the original axios error 168 | that was thrown: 169 | 170 | ``` 171 | originalError - Error - the error that axios threw in case you need more info 172 | ``` 173 | 174 | ## Changing Base URL 175 | 176 | You can change the URL your api is connecting to. 177 | 178 | ```js 179 | api.setBaseURL('https://some.other.place.com/api/v100') 180 | console.log(`omg i am now at ${api.getBaseURL()}`) 181 | ``` 182 | 183 | ## Changing Headers 184 | 185 | Once you've created your api, you're able to change HTTP requests by 186 | calling `setHeader` or `setHeaders` on the api. These stay with the api instance, so you can just set ['em and forget 'em](https://gitter.im/infinitered/ignite?at=582e57563f3946057acd2f84). 187 | 188 | ```js 189 | api.setHeader('Authorization', 'the new token goes here') 190 | api.setHeaders({ 191 | Authorization: 'token', 192 | 'X-Even-More': 'hawtness', 193 | }) 194 | ``` 195 | 196 | ## Adding Monitors 197 | 198 | Monitors are functions you can attach to the API which will be called 199 | when any request is made. You can use it to do things like: 200 | 201 | - check for headers and record values 202 | - determine if you need to trigger other parts of your code 203 | - measure performance of API calls 204 | - perform logging 205 | 206 | Monitors are run just before the promise is resolved. You get an 207 | early sneak peak at what will come back. 208 | 209 | You cannot change anything, just look. 210 | 211 | Here's a sample monitor: 212 | 213 | ```js 214 | const naviMonitor = response => console.log('hey! listen! ', response) 215 | api.addMonitor(naviMonitor) 216 | ``` 217 | 218 | Any exceptions that you trigger in your monitor will not affect the flow 219 | of the api request. 220 | 221 | ```js 222 | api.addMonitor(response => this.kaboom()) 223 | ``` 224 | 225 | Internally, each monitor callback is surrounded by an oppressive `try/catch` 226 | block. 227 | 228 | Remember. Safety first! 229 | 230 | ## Adding Transforms 231 | 232 | In addition to monitoring, you can change every request or response globally. 233 | 234 | This can be useful if you would like to: 235 | 236 | - fix an api response 237 | - add/edit/delete query string variables for all requests 238 | - change outbound headers without changing everywhere in your app 239 | 240 | Unlike monitors, exceptions are not swallowed. They will bring down the stack, so be careful! 241 | 242 | ### Response Transforms 243 | 244 | For responses, you're provided an object with these properties. 245 | 246 | - `data` - the object originally from the server that you might wanna mess with 247 | - `duration` - the number of milliseconds 248 | - `problem` - the problem code (see the bottom for the list) 249 | - `ok` - true or false 250 | - `status` - the HTTP status code 251 | - `headers` - the HTTP response headers 252 | - `config` - the underlying axios config for the request 253 | 254 | Data is the only option changeable. 255 | 256 | ```js 257 | api.addResponseTransform(response => { 258 | const badluck = Math.floor(Math.random() * 10) === 0 259 | if (badluck) { 260 | // just mutate the data to what you want. 261 | response.data.doorsOpen = false 262 | response.data.message = 'I cannot let you do that.' 263 | } 264 | }) 265 | ``` 266 | 267 | Or make it async: 268 | 269 | ```js 270 | api.addAsyncResponseTransform(async response => { 271 | const something = await AsyncStorage.load('something') 272 | if (something) { 273 | // just mutate the data to what you want. 274 | response.data.doorsOpen = false 275 | response.data.message = 'I cannot let you do that.' 276 | } 277 | }) 278 | ``` 279 | 280 | ### Request Transforms 281 | 282 | For requests, you are given a `request` object. Mutate anything in here to change anything about the request. 283 | 284 | The object passed in has these properties: 285 | 286 | - `data` - the object being passed up to the server 287 | - `method` - the HTTP verb 288 | - `url` - the url we're hitting 289 | - `headers` - the request headers 290 | - `params` - the request params for `get`, `delete`, `head`, `link`, `unlink` 291 | 292 | Request transforms can be a function: 293 | 294 | ```js 295 | api.addRequestTransform(request => { 296 | request.headers['X-Request-Transform'] = 'Changing Stuff!' 297 | request.params['page'] = 42 298 | delete request.params.secure 299 | request.url = request.url.replace(/\/v1\//, '/v2/') 300 | if (request.data.password && request.data.password === 'password') { 301 | request.data.username = `${request.data.username} is secure!` 302 | } 303 | }) 304 | ``` 305 | 306 | And you can also add an async version for use with Promises or `async/await`. When you resolve 307 | your promise, ensure you pass the request along. 308 | 309 | ```js 310 | api.addAsyncRequestTransform(request => { 311 | return new Promise(resolve => setTimeout(resolve, 2000)) 312 | }) 313 | ``` 314 | 315 | ```js 316 | api.addAsyncRequestTransform(request => async () => { 317 | await AsyncStorage.load('something') 318 | }) 319 | ``` 320 | 321 | This is great if you need to fetch an API key from storage for example. 322 | 323 | Multiple async transforms will be run one at a time in succession, not parallel. 324 | 325 | # Using Async/Await 326 | 327 | If you're more of a `stage-0` kinda person, you can use it like this: 328 | 329 | ```js 330 | const api = create({ baseURL: '...' }) 331 | const response = await api.get('/slowest/site/on/the/net') 332 | console.log(response.ok) // yay! 333 | ``` 334 | 335 | # Cancel Request 336 | 337 | ```js 338 | import { CancelToken } from 'apisauce' 339 | 340 | const source = CancelToken.source() 341 | const api = create({ baseURL: 'github.com' }) 342 | api.get('/users', {}, { cancelToken: source.token }) 343 | 344 | // To cancel request 345 | source.cancel() 346 | ``` 347 | 348 | # Problem Codes 349 | 350 | The `problem` property on responses is filled with the best 351 | guess on where the problem lies. You can use a switch to 352 | check the problem. The values are exposed as `CONSTANTS` 353 | hanging on your built API. 354 | 355 | ``` 356 | Constant VALUE Status Code Explanation 357 | ---------------------------------------------------------------------------------------- 358 | NONE null 200-299 No problems. 359 | CLIENT_ERROR 'CLIENT_ERROR' 400-499 Any non-specific 400 series error. 360 | SERVER_ERROR 'SERVER_ERROR' 500-599 Any 500 series error. 361 | TIMEOUT_ERROR 'TIMEOUT_ERROR' --- Server didn't respond in time. 362 | CONNECTION_ERROR 'CONNECTION_ERROR' --- Server not available, bad dns. 363 | NETWORK_ERROR 'NETWORK_ERROR' --- Network not available. 364 | CANCEL_ERROR 'CANCEL_ERROR' --- Request has been cancelled. Only possible if `cancelToken` is provided in config, see axios `Cancellation`. 365 | ``` 366 | 367 | Which problem is chosen will be picked by walking down the list. 368 | 369 | # Mocking with axios-mock-adapter (or other libraries) 370 | 371 | A common testing pattern is to use `axios-mock-adapter` to mock axios and respond with stubbed data. These libraries mock a specific instance of axios, and don't globally intercept all instances of axios. When using a mocking library like this, it's important to make sure to pass the same axios instance into the mock adapter. 372 | 373 | Here is an example code from axios_mock, modified to work with Apisauce: 374 | 375 | ```diff 376 | import apisauce from 'apisauce' 377 | import MockAdapter from 'axios-mock-adapter' 378 | 379 | test('mock adapter', async () => { 380 | const api = apisauce.create("https://api.github.com") 381 | - const mock = new MockAdapter(axios) 382 | + const mock = new MockAdapter(api.axiosInstance) 383 | mock.onGet("/repos/skellock/apisauce/commits").reply(200, { 384 | commits: [{ id: 1, sha: "aef849923444" }], 385 | }); 386 | 387 | const response = await api..get('/repos/skellock/apisauce/commits') 388 | expect(response.data[0].sha).toEqual"aef849923444") 389 | }) 390 | ``` 391 | 392 | # Contributing 393 | 394 | Bugs? Comments? Features? PRs and Issues happily welcomed! Make sure to check out our [contributing guide](./github/CONTRIBUTING.md) to get started! 395 | -------------------------------------------------------------------------------- /apisauce.d.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance, AxiosRequestConfig, AxiosError, CancelTokenStatic } from 'axios' 2 | 3 | export type HEADERS = { [key: string]: string } 4 | export const DEFAULT_HEADERS: { 5 | Accept: 'application/json' 6 | 'Content-Type': 'application/json' 7 | } 8 | 9 | export const NONE: null 10 | export const CLIENT_ERROR: 'CLIENT_ERROR' 11 | export const SERVER_ERROR: 'SERVER_ERROR' 12 | export const TIMEOUT_ERROR: 'TIMEOUT_ERROR' 13 | export const CONNECTION_ERROR: 'CONNECTION_ERROR' 14 | export const NETWORK_ERROR: 'NETWORK_ERROR' 15 | export const UNKNOWN_ERROR: 'UNKNOWN_ERROR' 16 | export const CANCEL_ERROR: 'CANCEL_ERROR' 17 | export type PROBLEM_CODE = 18 | | 'CLIENT_ERROR' 19 | | 'SERVER_ERROR' 20 | | 'TIMEOUT_ERROR' 21 | | 'CONNECTION_ERROR' 22 | | 'NETWORK_ERROR' 23 | | 'UNKNOWN_ERROR' 24 | | 'CANCEL_ERROR' 25 | 26 | export interface ApisauceConfig extends AxiosRequestConfig { 27 | baseURL: string | undefined 28 | axiosInstance?: AxiosInstance 29 | } 30 | 31 | /** 32 | * Creates a instance of our API using the configuration 33 | * @param config a configuration object which must have a non-empty 'baseURL' property. 34 | */ 35 | export function create(config: ApisauceConfig): ApisauceInstance 36 | 37 | export interface ApiErrorResponse { 38 | ok: false 39 | problem: PROBLEM_CODE 40 | originalError: AxiosError 41 | 42 | data?: T 43 | status?: number 44 | headers?: HEADERS 45 | config?: AxiosRequestConfig 46 | duration?: number 47 | } 48 | export interface ApiOkResponse { 49 | ok: true 50 | problem: null 51 | originalError: null 52 | 53 | data?: T 54 | status?: number 55 | headers?: HEADERS 56 | config?: AxiosRequestConfig 57 | duration?: number 58 | } 59 | export type ApiResponse = ApiErrorResponse | ApiOkResponse 60 | 61 | export type Monitor = (response: ApiResponse) => void 62 | export type RequestTransform = (request: AxiosRequestConfig) => void 63 | export type AsyncRequestTransform = ( 64 | request: AxiosRequestConfig, 65 | ) => Promise | ((request: AxiosRequestConfig) => Promise) 66 | export type ResponseTransform = (response: ApiResponse) => void 67 | export type AsyncResponseTransform = ( 68 | response: ApiResponse, 69 | ) => Promise | ((response: ApiResponse) => Promise) 70 | 71 | export interface ApisauceInstance { 72 | axiosInstance: AxiosInstance 73 | 74 | monitors: Monitor 75 | addMonitor: (monitor: Monitor) => void 76 | 77 | requestTransforms: RequestTransform[] 78 | asyncRequestTransforms: AsyncRequestTransform[] 79 | responseTransforms: ResponseTransform[] 80 | asyncResponseTransforms: AsyncResponseTransform[] 81 | addRequestTransform: (transform: RequestTransform) => void 82 | addAsyncRequestTransform: (transform: AsyncRequestTransform) => void 83 | addResponseTransform: (transform: ResponseTransform) => void 84 | addAsyncResponseTransform: (transform: AsyncResponseTransform) => void 85 | 86 | headers: HEADERS 87 | setHeader: (key: string, value: string) => AxiosInstance 88 | setHeaders: (headers: HEADERS) => AxiosInstance 89 | deleteHeader: (name: string) => AxiosInstance 90 | 91 | /** Sets a new base URL */ 92 | setBaseURL: (baseUrl: string) => AxiosInstance 93 | /** Gets the current base URL used by axios */ 94 | getBaseURL: () => string 95 | 96 | any: (config: AxiosRequestConfig) => Promise> 97 | get: (url: string, params?: {}, axiosConfig?: AxiosRequestConfig) => Promise> 98 | delete: (url: string, params?: {}, axiosConfig?: AxiosRequestConfig) => Promise> 99 | head: (url: string, params?: {}, axiosConfig?: AxiosRequestConfig) => Promise> 100 | post: (url: string, data?: any, axiosConfig?: AxiosRequestConfig) => Promise> 101 | put: (url: string, data?: any, axiosConfig?: AxiosRequestConfig) => Promise> 102 | patch: (url: string, data?: any, axiosConfig?: AxiosRequestConfig) => Promise> 103 | link: (url: string, params?: {}, axiosConfig?: AxiosRequestConfig) => Promise> 104 | unlink: (url: string, params?: {}, axiosConfig?: AxiosRequestConfig) => Promise> 105 | } 106 | 107 | export function isCancel(value: any): boolean 108 | 109 | export const CancelToken: CancelTokenStatic 110 | 111 | declare const _default: { 112 | DEFAULT_HEADERS: typeof DEFAULT_HEADERS 113 | NONE: typeof NONE 114 | CLIENT_ERROR: typeof CLIENT_ERROR 115 | SERVER_ERROR: typeof SERVER_ERROR 116 | TIMEOUT_ERROR: typeof TIMEOUT_ERROR 117 | CONNECTION_ERROR: typeof CONNECTION_ERROR 118 | NETWORK_ERROR: typeof NETWORK_ERROR 119 | UNKNOWN_ERROR: typeof UNKNOWN_ERROR 120 | create: typeof create 121 | isCancel: typeof isCancel 122 | CancelToken: typeof CancelToken 123 | } 124 | 125 | export default _default 126 | -------------------------------------------------------------------------------- /examples/github.js: -------------------------------------------------------------------------------- 1 | const apisauce = require('../dist/apisauce.js') 2 | 3 | const REPO = 'infinitered/apisauce' 4 | 5 | const api = apisauce.create({ 6 | baseURL: 'https://api.github.com', 7 | headers: { 8 | Accept: 'application/vnd.github.v3+json', 9 | }, 10 | }) 11 | 12 | // attach a monitor that fires with each request 13 | api.addMonitor(response => { 14 | const info = `Calls remaining this hour: ${response.headers['x-ratelimit-remaining']}` 15 | console.log(info) 16 | }) 17 | 18 | // show the latest commit message 19 | api.get(`/repos/${REPO}/commits`).then(response => { 20 | const info = `Latest Commit: ${response.data[0].commit.message}` 21 | console.log(info) 22 | }) 23 | 24 | // call a non-existant API to show that the flow is identical! 25 | api.post('/something/bad').then(({ ok, status, problem }) => { 26 | console.log({ ok, status, problem }) 27 | }) 28 | -------------------------------------------------------------------------------- /github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Apisauce 2 | 3 | We welcome all contributions! This guide should help you get up and running to submit your first pull request. 4 | 5 | ## Requirements 6 | 7 | - Yarn 1.7+ 8 | 9 | ## Getting Started 10 | 11 | 1. Fork and then clone the repo (`git clone git@github.com:/apisauce.git`) 12 | 2. cd into the directory (`cd apisauce`) 13 | 3. Install dependencies: `yarn` 14 | 4. Make your changes! You can test them out in a React or React Native project by installing from your github branch: `yarn install ` 15 | 16 | ## Before Submitting 17 | 18 | Before submitting a pull request, you will want to make sure your branch meets the following requirements: 19 | 20 | - Everything works on iOS/Android/Web 21 | - Unit tests are passing (`yarn test`) 22 | - New unit tests have been included for any new features or functionality 23 | - Code is compliant with the linter and typescript compiles (`yarn lint && yarn compile`) 24 | - Branch has been [synced with the upstream repo](https://help.github.com/articles/syncing-a-fork/) and any merge conflicts have been resolved. 25 | 26 | ## Submitting 27 | 28 | When you are at the stage of opening your pull request on GitHub, make sure you include the following info in the description: 29 | 30 | - The reasoning behind this change. What bug is it fixing? If it's a new feature, what need is it addressing, or value is it adding? 31 | - Any notes to help us review the code or test the change. 32 | - Any relevant screenshots or evidence of this feature in action. GIFs are particularly great to show new functionality. 33 | 34 | [Here](https://blog.github.com/2015-01-21-how-to-write-the-perfect-pull-request/) is a great guide from GitHub on how to write a great pull request! 35 | 36 | ## Git Hooks 37 | 38 | We use git hooks (via [Husky](https://github.com/typicode/husky)) to ensure our codebase remains properly formatted. 39 | 40 | The hooks will run the linter and the compiler on any staged code before committing. 41 | 42 | If you have to bypass the linter for a special commit that you will come back and clean (pushing something to a branch etc.) then you can bypass git hooks with adding `--no-verify` to your commit command. 43 | 44 | **Understanding Linting Errors** 45 | 46 | The linting rules are from TSLint. [TSLint errors can be found with descriptions here](https://palantir.github.io/tslint/rules/). 47 | -------------------------------------------------------------------------------- /github/labels.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "priority: critical", 4 | "color": "#e11d21" 5 | }, 6 | { 7 | "name": "priority: high", 8 | "color": "#eb6420" 9 | }, 10 | { 11 | "name": "priority: low", 12 | "color": "#d2f0c6" 13 | }, 14 | { 15 | "name": "status: abandoned", 16 | "color": "#000000" 17 | }, 18 | { 19 | "name": "status: blocked", 20 | "color": "#e11d21" 21 | }, 22 | { 23 | "name": "status: help wanted", 24 | "color": "#006b75" 25 | }, 26 | { 27 | "name": "status: in progress", 28 | "color": "#cccccc" 29 | }, 30 | { 31 | "name": "status: on hold", 32 | "color": "#e11d21" 33 | }, 34 | { 35 | "name": "status: pending", 36 | "color": "#fef2c0" 37 | }, 38 | { 39 | "name": "status: review needed", 40 | "color": "#fbca04" 41 | }, 42 | { 43 | "name": "status: revision needed", 44 | "color": "#e11d21" 45 | }, 46 | { 47 | "name": "type: bug", 48 | "color": "#e11d21" 49 | }, 50 | { 51 | "name": "type: discussion", 52 | "color": "#0052cc" 53 | }, 54 | { 55 | "name": "type: docs", 56 | "color": "#cfe4f2" 57 | }, 58 | { 59 | "name": "type: enhancement", 60 | "color": "#84b6eb" 61 | }, 62 | { 63 | "name": "type: maintenance", 64 | "color": "#fbca04" 65 | }, 66 | { 67 | "name": "type: marketing", 68 | "color": "#d4c5f9" 69 | }, 70 | { 71 | "name": "type: question", 72 | "color": "#cc317c" 73 | } 74 | ] -------------------------------------------------------------------------------- /lib/apisauce.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosResponse, AxiosError } from 'axios' 2 | 3 | /** 4 | * Converts the parameter to a number. 5 | * 6 | * Number, null, and undefined will return themselves, 7 | * but everything else will be convert to a Number, or 8 | * die trying. 9 | * 10 | * @param {String} the String to convert 11 | * @return {Number} the Number version 12 | * @example 13 | * toNumber('7') //=> 7 14 | */ 15 | const toNumber = (value: any): number => { 16 | // if value is a Date, convert to a number 17 | if (value instanceof Date) { 18 | return value.getTime() 19 | } 20 | 21 | if (typeof value === 'number' || value === null || value === undefined) { 22 | return value 23 | } 24 | 25 | return Number(value) 26 | } 27 | 28 | /** 29 | * Given a min and max, determines if the value is included 30 | * in the range. 31 | * 32 | * @sig Number a -> a -> a -> b 33 | * @param {Number} the minimum number 34 | * @param {Number} the maximum number 35 | * @param {Number} the value to test 36 | * @return {Boolean} is the value in the range? 37 | * @example 38 | * isWithin(1, 5, 3) //=> true 39 | * isWithin(1, 5, 1) //=> true 40 | * isWithin(1, 5, 5) //=> true 41 | * isWithin(1, 5, 5.1) //=> false 42 | */ 43 | const isWithin = (min: number, max: number, value: number): boolean => value >= min && value <= max 44 | 45 | /** 46 | * Are we dealing with a promise? 47 | */ 48 | const isPromise = obj => 49 | !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function' 50 | 51 | // the default headers given to axios 52 | export const DEFAULT_HEADERS = { 53 | Accept: 'application/json', 54 | 'Content-Type': 'application/json', 55 | } 56 | 57 | // the default configuration for axios, default headers will also be merged in 58 | const DEFAULT_CONFIG = { 59 | timeout: 0, 60 | } 61 | 62 | export const NONE = null 63 | export const CLIENT_ERROR = 'CLIENT_ERROR' 64 | export const SERVER_ERROR = 'SERVER_ERROR' 65 | export const TIMEOUT_ERROR = 'TIMEOUT_ERROR' 66 | export const CONNECTION_ERROR = 'CONNECTION_ERROR' 67 | export const NETWORK_ERROR = 'NETWORK_ERROR' 68 | export const UNKNOWN_ERROR = 'UNKNOWN_ERROR' 69 | export const CANCEL_ERROR = 'CANCEL_ERROR' 70 | 71 | const TIMEOUT_ERROR_CODES = ['ECONNABORTED'] 72 | const NODEJS_CONNECTION_ERROR_CODES = ['ENOTFOUND', 'ECONNREFUSED', 'ECONNRESET'] 73 | const STATUS_ERROR_CODES = ['ERR_BAD_REQUEST', 'ERR_BAD_RESPONSE'] 74 | const in200s = (n: number): boolean => isWithin(200, 299, n) 75 | const in400s = (n: number): boolean => isWithin(400, 499, n) 76 | const in500s = (n: number): boolean => isWithin(500, 599, n) 77 | 78 | /** 79 | * What's the problem for this axios response? 80 | */ 81 | export const getProblemFromError = error => { 82 | // first check if the error message is Network Error (set by axios at 0.12) on platforms other than NodeJS. 83 | if (error.message === 'Network Error') return NETWORK_ERROR 84 | if (axios.isCancel(error)) return CANCEL_ERROR 85 | 86 | // then check the specific error code 87 | if (!error.code) return getProblemFromStatus(error.response ? error.response.status : null) 88 | if (STATUS_ERROR_CODES.includes(error.code)) return getProblemFromStatus(error.response.status) 89 | if (TIMEOUT_ERROR_CODES.includes(error.code)) return TIMEOUT_ERROR 90 | if (NODEJS_CONNECTION_ERROR_CODES.includes(error.code)) return CONNECTION_ERROR 91 | return UNKNOWN_ERROR 92 | } 93 | 94 | type StatusCodes = undefined | number 95 | 96 | /** 97 | * Given a HTTP status code, return back the appropriate problem enum. 98 | */ 99 | export const getProblemFromStatus = (status: StatusCodes) => { 100 | if (!status) return UNKNOWN_ERROR 101 | if (in200s(status)) return NONE 102 | if (in400s(status)) return CLIENT_ERROR 103 | if (in500s(status)) return SERVER_ERROR 104 | return UNKNOWN_ERROR 105 | } 106 | 107 | /** 108 | * Creates a instance of our API using the configuration. 109 | */ 110 | export const create = config => { 111 | // combine the user's headers with ours 112 | const headers = { ...DEFAULT_HEADERS, ...(config.headers || {}) } 113 | 114 | let instance 115 | if (config.axiosInstance) { 116 | // use passed axios instance 117 | instance = config.axiosInstance 118 | } else { 119 | const configWithoutHeaders = { ...config, headers: undefined } 120 | const combinedConfig = { ...DEFAULT_CONFIG, ...configWithoutHeaders } 121 | // create the axios instance 122 | instance = axios.create(combinedConfig) 123 | } 124 | 125 | const monitors = [] 126 | const addMonitor = monitor => { 127 | monitors.push(monitor) 128 | } 129 | 130 | const requestTransforms = [] 131 | const asyncRequestTransforms = [] 132 | const responseTransforms = [] 133 | const asyncResponseTransforms = [] 134 | 135 | const addRequestTransform = transform => requestTransforms.push(transform) 136 | const addAsyncRequestTransform = transform => asyncRequestTransforms.push(transform) 137 | const addResponseTransform = transform => responseTransforms.push(transform) 138 | const addAsyncResponseTransform = transform => asyncResponseTransforms.push(transform) 139 | 140 | // convenience for setting new request headers 141 | const setHeader = (name, value) => { 142 | headers[name] = value 143 | return instance 144 | } 145 | 146 | // sets headers in bulk 147 | const setHeaders = headers => { 148 | Object.keys(headers).forEach(header => setHeader(header, headers[header])) 149 | return instance 150 | } 151 | 152 | // remove header 153 | const deleteHeader = name => { 154 | delete headers[name] 155 | return instance 156 | } 157 | 158 | /** 159 | * Sets a new base URL. 160 | */ 161 | const setBaseURL = newURL => { 162 | instance.defaults.baseURL = newURL 163 | return instance 164 | } 165 | 166 | /** 167 | * Gets the current base URL used by axios. 168 | */ 169 | const getBaseURL = () => { 170 | return instance.defaults.baseURL 171 | } 172 | 173 | type RequestsWithoutBody = 'get' | 'head' | 'delete' | 'link' | 'unlink' 174 | type RequestsWithBody = 'post' | 'put' | 'patch' 175 | 176 | /** 177 | * Make the request for GET, HEAD, DELETE 178 | */ 179 | const doRequestWithoutBody = (method: RequestsWithoutBody) => (url: string, params = {}, axiosConfig = {}) => { 180 | return doRequest({ ...axiosConfig, url, params, method }) 181 | } 182 | 183 | /** 184 | * Make the request for POST, PUT, PATCH 185 | */ 186 | const doRequestWithBody = (method: RequestsWithBody) => (url: string, data, axiosConfig = {}) => { 187 | return doRequest({ ...axiosConfig, url, method, data }) 188 | } 189 | 190 | /** 191 | * Make the request with this config! 192 | */ 193 | const doRequest = async axiosRequestConfig => { 194 | axiosRequestConfig.headers = { 195 | ...headers, 196 | ...axiosRequestConfig.headers, 197 | } 198 | 199 | // add the request transforms 200 | if (requestTransforms.length > 0) { 201 | // overwrite our axios request with whatever our object looks like now 202 | // axiosRequestConfig = doRequestTransforms(requestTransforms, axiosRequestConfig) 203 | requestTransforms.forEach(transform => transform(axiosRequestConfig)) 204 | } 205 | 206 | // add the async request transforms 207 | if (asyncRequestTransforms.length > 0) { 208 | for (let index = 0; index < asyncRequestTransforms.length; index++) { 209 | const transform = asyncRequestTransforms[index](axiosRequestConfig) 210 | if (isPromise(transform)) { 211 | await transform 212 | } else { 213 | await transform(axiosRequestConfig) 214 | } 215 | } 216 | } 217 | 218 | // after the call, convert the axios response, then execute our monitors 219 | const startTime = toNumber(new Date()) 220 | const chain = async response => { 221 | const ourResponse = await convertResponse(startTime, response) 222 | return runMonitors(ourResponse) 223 | } 224 | 225 | return instance 226 | .request(axiosRequestConfig) 227 | .then(chain) 228 | .catch(chain) 229 | } 230 | 231 | /** 232 | * Fires after we convert from axios' response into our response. Exceptions 233 | * raised for each monitor will be ignored. 234 | */ 235 | const runMonitors = ourResponse => { 236 | monitors.forEach(monitor => { 237 | try { 238 | monitor(ourResponse) 239 | } catch (error) { 240 | // all monitor complaints will be ignored 241 | } 242 | }) 243 | return ourResponse 244 | } 245 | 246 | /** 247 | * Converts an axios response/error into our response. 248 | */ 249 | const convertResponse = async (startedAt: number, axiosResult: AxiosResponse | AxiosError) => { 250 | const end: number = toNumber(new Date()) 251 | const duration: number = end - startedAt 252 | 253 | // new in Axios 0.13 -- some data could be buried 1 level now 254 | const isError = axiosResult instanceof Error || axios.isCancel(axiosResult) 255 | const axiosResponse = axiosResult as AxiosResponse 256 | const axiosError = axiosResult as AxiosError 257 | const response = isError ? axiosError.response : axiosResponse 258 | const status = (response && response.status) || null 259 | const problem = isError ? getProblemFromError(axiosResult) : getProblemFromStatus(status) 260 | const originalError = isError ? axiosError : null 261 | const ok = in200s(status) 262 | const config = axiosResult.config || null 263 | const headers = (response && response.headers) || null 264 | let data = (response && response.data) ?? null 265 | 266 | // give an opportunity for anything to the response transforms to change stuff along the way 267 | let transformedResponse = { 268 | duration, 269 | problem, 270 | originalError, 271 | ok, 272 | status, 273 | headers, 274 | config, 275 | data, 276 | } 277 | if (responseTransforms.length > 0) { 278 | responseTransforms.forEach(transform => transform(transformedResponse)) 279 | } 280 | 281 | // add the async response transforms 282 | if (asyncResponseTransforms.length > 0) { 283 | for (let index = 0; index < asyncResponseTransforms.length; index++) { 284 | const transform = asyncResponseTransforms[index](transformedResponse) 285 | if (isPromise(transform)) { 286 | await transform 287 | } else { 288 | await transform(transformedResponse) 289 | } 290 | } 291 | } 292 | 293 | return transformedResponse 294 | } 295 | 296 | // create the base object 297 | const sauce = { 298 | axiosInstance: instance, 299 | monitors, 300 | addMonitor, 301 | requestTransforms, 302 | asyncRequestTransforms, 303 | responseTransforms, 304 | asyncResponseTransforms, 305 | addRequestTransform, 306 | addAsyncRequestTransform, 307 | addResponseTransform, 308 | addAsyncResponseTransform, 309 | setHeader, 310 | setHeaders, 311 | deleteHeader, 312 | headers, 313 | setBaseURL, 314 | getBaseURL, 315 | any: doRequest, 316 | get: doRequestWithoutBody('get'), 317 | delete: doRequestWithoutBody('delete'), 318 | head: doRequestWithoutBody('head'), 319 | post: doRequestWithBody('post'), 320 | put: doRequestWithBody('put'), 321 | patch: doRequestWithBody('patch'), 322 | link: doRequestWithoutBody('link'), 323 | unlink: doRequestWithoutBody('unlink'), 324 | } 325 | // send back the sauce 326 | return sauce 327 | } 328 | 329 | export const { isCancel, CancelToken } = axios 330 | 331 | export default { 332 | DEFAULT_HEADERS, 333 | NONE, 334 | CLIENT_ERROR, 335 | SERVER_ERROR, 336 | TIMEOUT_ERROR, 337 | CONNECTION_ERROR, 338 | NETWORK_ERROR, 339 | UNKNOWN_ERROR, 340 | create, 341 | isCancel, 342 | CancelToken, 343 | } 344 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3.1.1", 3 | "author": { 4 | "name": "Infinite Red", 5 | "email": "npm@infinite.red", 6 | "url": "https://github.com/infinitered/ignite" 7 | }, 8 | "ava": { 9 | "require": [ 10 | "babel-core/register" 11 | ] 12 | }, 13 | "dependencies": { 14 | "axios": "^1.7.7" 15 | }, 16 | "description": "Axios + standardized errors + request/response transforms.", 17 | "devDependencies": { 18 | "@semantic-release/git": "^7.0.5", 19 | "@types/node": "14.0.4", 20 | "ava": "0.25.0", 21 | "babel-cli": "^6.26.0", 22 | "babel-core": "^6.26.3", 23 | "babel-eslint": "^8.2.3", 24 | "babel-preset-es2015": "^6.24.1", 25 | "husky": "^1.3.1", 26 | "lint-staged": "^8.1.0", 27 | "np": "3.0.4", 28 | "npm-run-all": "^4.1.5", 29 | "nyc": "^11.8.0", 30 | "prettier": "^1.15.3", 31 | "rollup": "^0.59.1", 32 | "rollup-plugin-babel": "^3.0.4", 33 | "rollup-plugin-commonjs": "^8.4.1", 34 | "rollup-plugin-filesize": "^1.5.0", 35 | "rollup-plugin-node-resolve": "^3.2.0", 36 | "rollup-plugin-uglify": "^3.0.0", 37 | "semantic-release": "^15.12.4", 38 | "tslint": "^6.1.3", 39 | "tslint-config-prettier": "^1.17.0", 40 | "tslint-config-standard": "^9.0.0", 41 | "typescript": "5.1.3" 42 | }, 43 | "files": [ 44 | "dist/apisauce.js", 45 | "dist/umd/apisauce.js", 46 | "apisauce.d.ts" 47 | ], 48 | "keywords": [ 49 | "axios", 50 | "api", 51 | "network", 52 | "http" 53 | ], 54 | "license": "MIT", 55 | "main": "./dist/apisauce.js", 56 | "name": "apisauce", 57 | "repository": { 58 | "type": "git", 59 | "url": "https://github.com/infinitered/apisauce.git" 60 | }, 61 | "scripts": { 62 | "build": "BABEL_ENV=production rollup -c", 63 | "clean": "rm -rf dist", 64 | "compile": "tsc -p tsconfig.json", 65 | "coverage": "nyc ava", 66 | "prepare": "npm-run-all compile build", 67 | "dist": "npm-run-all clean compile build test", 68 | "lint": "tslint -p .", 69 | "test": "npm-run-all compile test:unit", 70 | "test:unit": "ava -s", 71 | "ci:publish": "yarn semantic-release", 72 | "semantic-release": "semantic-release", 73 | "format": "prettier --write \"{**/*.ts,.circleci/**/*.js}\" --loglevel error && tslint -p . --fix", 74 | "example": "node ./examples/github.js" 75 | }, 76 | "prettier": { 77 | "semi": false, 78 | "singleQuote": true, 79 | "trailingComma": "all", 80 | "printWidth": 120 81 | }, 82 | "lint-staged": { 83 | "*.ts": [ 84 | "prettier --write", 85 | "tslint --fix", 86 | "git add" 87 | ], 88 | "*.md": [ 89 | "prettier --write", 90 | "git add" 91 | ], 92 | "*.json": [ 93 | "prettier --write", 94 | "git add" 95 | ] 96 | }, 97 | "types": "./apisauce.d.ts", 98 | "release": { 99 | "plugins": [ 100 | "@semantic-release/commit-analyzer", 101 | "@semantic-release/release-notes-generator", 102 | "@semantic-release/npm", 103 | "@semantic-release/github", 104 | [ 105 | "@semantic-release/git", 106 | { 107 | "assets": "package.json", 108 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 109 | } 110 | ] 111 | ] 112 | }, 113 | "husky": { 114 | "hooks": { 115 | "pre-commit": "lint-staged" 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import filesize from 'rollup-plugin-filesize' 4 | import resolve from 'rollup-plugin-node-resolve' 5 | import uglify from 'rollup-plugin-uglify' 6 | 7 | const externalModules = ['axios'] 8 | const input = 'lib/apisauce.js' 9 | const name = 'apisauce' 10 | 11 | function isImportExternal(importStr) { 12 | let external = false 13 | 14 | // Check for each of the external modules defined above 15 | externalModules.forEach(externalModule => { 16 | if (importStr.indexOf(externalModule) >= 0) external = true 17 | }) 18 | 19 | return external 20 | } 21 | 22 | function getBabelOptions() { 23 | return { babelrc: false, plugins: [] } 24 | } 25 | 26 | export default [ 27 | { 28 | input, 29 | output: { 30 | exports: 'named', 31 | file: `dist/${name}.js`, 32 | format: 'cjs', 33 | }, 34 | plugins: [babel(getBabelOptions()), uglify(), filesize()], 35 | external: isImportExternal, 36 | }, 37 | { 38 | input, 39 | output: { 40 | exports: 'named', 41 | file: `dist/umd/${name}.js`, 42 | format: 'umd', 43 | globals: { 44 | axios: 'axios', 45 | }, 46 | name, 47 | }, 48 | plugins: [babel(getBabelOptions()), resolve(), commonjs(), uglify(), filesize()], 49 | external: externalModules, 50 | }, 51 | ] 52 | -------------------------------------------------------------------------------- /test/_getFreePort.js: -------------------------------------------------------------------------------- 1 | import net from 'net' 2 | 3 | export default function getFreePort (cb) { 4 | return new Promise(resolve => { 5 | const server = net.createServer() 6 | server.listen(() => { 7 | const port = server.address().port 8 | server.close(() => resolve(port)) 9 | }) 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /test/_server.js: -------------------------------------------------------------------------------- 1 | 2 | import http from 'http' 3 | 4 | const processPost = (request, response, callback) => { 5 | let queryData = '' 6 | if (typeof callback !== 'function') return null 7 | 8 | request.on('data', function(data) { 9 | queryData += data 10 | }) 11 | 12 | request.on('end', function() { 13 | request.post = JSON.parse(queryData) 14 | callback() 15 | }) 16 | } 17 | 18 | const sendResponse = (res, statusCode, body) => { 19 | res.writeHead(statusCode) 20 | res.write(body) 21 | res.end() 22 | } 23 | 24 | const send200 = (res, body) => { 25 | sendResponse(res, 200, body || '

OK

') 26 | } 27 | 28 | export default (port, mockData = {}) => { 29 | return new Promise(resolve => { 30 | const server = http.createServer((req, res) => { 31 | const url = req.url 32 | if (url === '/ok') { 33 | send200(res) 34 | return 35 | } 36 | 37 | if (url.startsWith('/echo')) { 38 | const echo = url.slice(8) 39 | sendResponse(res, 200, JSON.stringify({ echo })) 40 | return 41 | } 42 | 43 | if (url.startsWith('/number')) { 44 | const n = url.slice(8, 11) 45 | sendResponse(res, n, JSON.stringify(mockData)) 46 | return 47 | } 48 | 49 | if (url.startsWith('/falsy')) { 50 | sendResponse(res, 200, JSON.stringify(false)) 51 | return 52 | } 53 | 54 | if (url.startsWith('/sleep')) { 55 | const wait = Number(url.split('/').pop()) 56 | setTimeout(() => { 57 | send200(res) 58 | }, wait) 59 | return 60 | } 61 | 62 | if (url === '/post') { 63 | processPost(req, res, function() { 64 | sendResponse(res, 200, JSON.stringify({ got: req.post })) 65 | }) 66 | } 67 | }) 68 | server.listen(port, '::', () => resolve(server)) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /test/async-request-transform.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { create } from '../lib/apisauce' 3 | import createServer from './_server' 4 | import getFreePort from './_getFreePort' 5 | 6 | const MOCK = { b: 1 } 7 | let port 8 | let server = null 9 | 10 | /** 11 | * Wait before firing. 12 | * 13 | * @param {Number} time The number of milliseconds to wait. 14 | * @return {Promise} 15 | */ 16 | const delay = time => 17 | new Promise(resolve => { 18 | setTimeout(resolve, time) 19 | }) 20 | 21 | test.before(async t => { 22 | port = await getFreePort() 23 | server = await createServer(port, MOCK) 24 | }) 25 | 26 | test.after('cleanup', t => { 27 | server.close() 28 | }) 29 | 30 | test('attaches an async request transform', t => { 31 | const api = create({ baseURL: `http://localhost:${port}` }) 32 | t.truthy(api.addAsyncRequestTransform) 33 | t.truthy(api.asyncRequestTransforms) 34 | t.is(api.asyncRequestTransforms.length, 0) 35 | api.addAsyncRequestTransform(request => request) 36 | t.is(api.asyncRequestTransforms.length, 1) 37 | }) 38 | 39 | test('alters the request data', t => { 40 | const x = create({ baseURL: `http://localhost:${port}` }) 41 | let count = 0 42 | x.addAsyncRequestTransform(req => { 43 | return new Promise((resolve, reject) => { 44 | setImmediate(_ => { 45 | t.is(count, 0) 46 | count = 1 47 | t.is(req.data.b, 1) 48 | req.data.b = 2 49 | resolve(req) 50 | }) 51 | }) 52 | }) 53 | return x.post('/post', MOCK).then(response => { 54 | t.is(response.status, 200) 55 | t.is(count, 1) 56 | t.is(response.data.got.b, 2) 57 | }) 58 | }) 59 | 60 | test('serial async', async t => { 61 | const api = create({ baseURL: `http://localhost:${port}` }) 62 | let fired = false 63 | api.addAsyncRequestTransform(request => async () => { 64 | await delay(300) 65 | request.url = '/number/201' 66 | fired = true 67 | }) 68 | const response = await api.get('/number/200') 69 | t.true(response.ok) 70 | t.is(response.status, 201) 71 | t.true(fired) 72 | }) 73 | 74 | test('transformers should run serially', t => { 75 | const x = create({ baseURL: `http://localhost:${port}` }) 76 | let first = false 77 | let second = false 78 | x.addAsyncRequestTransform(req => { 79 | return new Promise((resolve, reject) => { 80 | setImmediate(_ => { 81 | t.is(second, false) 82 | t.is(first, false) 83 | first = true 84 | resolve() 85 | }) 86 | }) 87 | }) 88 | x.addAsyncRequestTransform(req => { 89 | return new Promise((resolve, reject) => { 90 | setImmediate(_ => { 91 | t.is(first, true) 92 | t.is(second, false) 93 | second = true 94 | resolve() 95 | }) 96 | }) 97 | }) 98 | return x.post('/post', MOCK).then(response => { 99 | t.is(response.status, 200) 100 | t.is(first, true) 101 | t.is(second, true) 102 | }) 103 | }) 104 | 105 | test('survives empty PUTs', t => { 106 | const x = create({ baseURL: `http://localhost:${port}` }) 107 | let count = 0 108 | x.addAsyncRequestTransform(req => { 109 | return new Promise((resolve, reject) => { 110 | setImmediate(_ => { 111 | count++ 112 | resolve() 113 | }) 114 | }) 115 | }) 116 | t.is(count, 0) 117 | return x.put('/post', {}).then(response => { 118 | t.is(response.status, 200) 119 | t.is(count, 1) 120 | }) 121 | }) 122 | 123 | test('fires for gets', t => { 124 | const x = create({ baseURL: `http://localhost:${port}` }) 125 | let count = 0 126 | x.addAsyncRequestTransform(req => { 127 | return new Promise((resolve, reject) => { 128 | setImmediate(_ => { 129 | count++ 130 | resolve() 131 | }) 132 | }) 133 | }) 134 | t.is(count, 0) 135 | return x.get('/number/201').then(response => { 136 | t.is(response.status, 201) 137 | t.is(count, 1) 138 | t.deepEqual(response.data, MOCK) 139 | }) 140 | }) 141 | 142 | test('url can be changed', t => { 143 | const x = create({ baseURL: `http://localhost:${port}` }) 144 | x.addAsyncRequestTransform(req => { 145 | return new Promise((resolve, reject) => { 146 | setImmediate(_ => { 147 | req.url = req.url.replace('/201', '/200') 148 | resolve() 149 | }) 150 | }) 151 | }) 152 | return x.get('/number/201', { x: 1 }).then(response => { 153 | t.is(response.status, 200) 154 | }) 155 | }) 156 | 157 | test('params can be added, edited, and deleted', t => { 158 | const x = create({ baseURL: `http://localhost:${port}` }) 159 | x.addAsyncRequestTransform(req => { 160 | return new Promise((resolve, reject) => { 161 | setImmediate(_ => { 162 | req.params.x = 2 163 | req.params.y = 1 164 | delete req.params.z 165 | resolve() 166 | }) 167 | }) 168 | }) 169 | return x.get('/number/200', { x: 1, z: 4 }).then(response => { 170 | t.is(response.status, 200) 171 | t.is(response.config.params.x, 2) 172 | t.is(response.config.params.y, 1) 173 | t.falsy(response.config.params.z) 174 | }) 175 | }) 176 | 177 | test('headers can be created', t => { 178 | const x = create({ baseURL: `http://localhost:${port}` }) 179 | x.addAsyncRequestTransform(req => { 180 | return new Promise((resolve, reject) => { 181 | setImmediate(_ => { 182 | t.falsy(req.headers['X-APISAUCE']) 183 | req.headers['X-APISAUCE'] = 'new' 184 | resolve() 185 | }) 186 | }) 187 | }) 188 | return x.get('/number/201', { x: 1 }).then(response => { 189 | t.is(response.status, 201) 190 | t.is(response.config.headers['X-APISAUCE'], 'new') 191 | }) 192 | }) 193 | 194 | test('headers from creation time can be changed', t => { 195 | const x = create({ 196 | baseURL: `http://localhost:${port}`, 197 | headers: { 'X-APISAUCE': 'hello' }, 198 | }) 199 | x.addAsyncRequestTransform(req => { 200 | return new Promise((resolve, reject) => { 201 | setImmediate(_ => { 202 | t.is(req.headers['X-APISAUCE'], 'hello') 203 | req.headers['X-APISAUCE'] = 'change' 204 | resolve() 205 | }) 206 | }) 207 | }) 208 | return x.get('/number/201', { x: 1 }).then(response => { 209 | t.is(response.status, 201) 210 | t.is(response.config.headers['X-APISAUCE'], 'change') 211 | }) 212 | }) 213 | 214 | test('headers can be deleted', t => { 215 | const x = create({ 216 | baseURL: `http://localhost:${port}`, 217 | headers: { 'X-APISAUCE': 'omg' }, 218 | }) 219 | x.addAsyncRequestTransform(req => { 220 | return new Promise((resolve, reject) => { 221 | setImmediate(_ => { 222 | t.is(req.headers['X-APISAUCE'], 'omg') 223 | delete req.headers['X-APISAUCE'] 224 | resolve() 225 | }) 226 | }) 227 | }) 228 | return x.get('/number/201', { x: 1 }).then(response => { 229 | t.is(response.status, 201) 230 | t.falsy(response.config.headers['X-APISAUCE']) 231 | }) 232 | }) 233 | -------------------------------------------------------------------------------- /test/async-response-transform.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { create } from '../lib/apisauce' 3 | import createServer from './_server' 4 | import getFreePort from './_getFreePort' 5 | 6 | const MOCK = { a: { b: [1, 2, 3] } } 7 | let port 8 | let server = null 9 | test.before(async t => { 10 | port = await getFreePort() 11 | server = await createServer(port, MOCK) 12 | }) 13 | 14 | test.after('cleanup', t => { 15 | server.close() 16 | }) 17 | 18 | test('attaches a async response transform', t => { 19 | const api = create({ baseURL: `http://localhost:${port}` }) 20 | 21 | console.log(api.asyncResponseTransforms) 22 | t.truthy(api.addAsyncResponseTransform) 23 | t.truthy(api.asyncResponseTransforms) 24 | t.is(api.asyncResponseTransforms.length, 0) 25 | api.addAsyncResponseTransform(data => data) 26 | t.is(api.asyncResponseTransforms.length, 1) 27 | }) 28 | 29 | test('alters the response', t => { 30 | const x = create({ baseURL: `http://localhost:${port}` }) 31 | let count = 0 32 | x.addAsyncResponseTransform(({ data }) => { 33 | return new Promise((resolve, reject) => { 34 | setImmediate(_ => { 35 | count++ 36 | data.a = 'hi' 37 | resolve(data) 38 | }) 39 | }) 40 | }) 41 | t.is(count, 0) 42 | return x.get('/number/201').then(response => { 43 | t.is(response.status, 201) 44 | t.is(count, 1) 45 | t.deepEqual(response.data.a, 'hi') 46 | }) 47 | }) 48 | 49 | test('swap out data on response', t => { 50 | const x = create({ baseURL: `http://localhost:${port}` }) 51 | let count = 0 52 | x.addAsyncResponseTransform(response => { 53 | return new Promise((resolve, reject) => { 54 | setImmediate(_ => { 55 | count++ 56 | response.status = 222 57 | response.data = { a: response.data.a.b.reverse() } 58 | resolve(response) 59 | }) 60 | }) 61 | }) 62 | // t.is(count, 0) 63 | return x.get('/number/201').then(response => { 64 | t.is(response.status, 222) 65 | t.is(count, 1) 66 | t.deepEqual(response.data.a, [3, 2, 1]) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /test/async.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { create } from '../lib/apisauce' 3 | import createServer from './_server' 4 | import getFreePort from './_getFreePort' 5 | 6 | let port 7 | let server = null 8 | const MOCK = { a: { b: [1, 2, 3] } } 9 | test.before(async t => { 10 | port = await getFreePort() 11 | server = await createServer(port, MOCK) 12 | }) 13 | 14 | test.after.always('cleanup', t => { 15 | server.close() 16 | }) 17 | 18 | test('can be used with async/await', async t => { 19 | const x = create({ baseURL: `http://localhost:${port}` }) 20 | const response = await x.get('/number/200', { a: 'b' }) 21 | t.is(response.status, 200) 22 | t.deepEqual(response.data, MOCK) 23 | }) 24 | -------------------------------------------------------------------------------- /test/cancellation.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { CancelToken, isCancel, create, CANCEL_ERROR } from '../lib/apisauce' 3 | import createServer from './_server' 4 | import getFreePort from './_getFreePort' 5 | 6 | let port 7 | let server = null 8 | test.before(async t => { 9 | port = await getFreePort() 10 | server = await createServer(port) 11 | }) 12 | 13 | test.after('cleanup', t => { 14 | server.close() 15 | }) 16 | 17 | test('cancel request', t => { 18 | const source = CancelToken.source() 19 | const x = create({ 20 | baseURL: `http://localhost:${port}`, 21 | cancelToken: source.token, 22 | timeout: 200, 23 | }) 24 | setTimeout(() => { 25 | source.cancel() 26 | }, 20) 27 | 28 | return x.get('/sleep/150').then(response => { 29 | t.truthy(isCancel(response.originalError)) 30 | t.falsy(response.ok) 31 | t.is(response.problem, CANCEL_ERROR) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/config.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { create, DEFAULT_HEADERS } from '../lib/apisauce' 3 | import axios from 'axios' 4 | 5 | const validConfig = { 6 | baseURL: 'http://localhost:9991', 7 | headers: { 'X-Testing': 'hello' }, 8 | } 9 | 10 | test('is a function', t => { 11 | t.is(typeof create, 'function') 12 | }) 13 | 14 | test('returns an object when we configure correctly', t => { 15 | const x = create(validConfig) 16 | t.truthy(x) 17 | t.truthy(x.axiosInstance) 18 | }) 19 | 20 | test('configures axios correctly', t => { 21 | const apisauce = create(validConfig) 22 | const { axiosInstance } = apisauce 23 | t.is(axiosInstance.defaults.timeout, 0) 24 | t.is(axiosInstance.defaults.baseURL, validConfig.baseURL) 25 | t.deepEqual(apisauce.headers, Object.assign({}, DEFAULT_HEADERS, validConfig.headers)) 26 | }) 27 | 28 | test('configures axios correctly with passed instance', t => { 29 | const customAxiosInstance = axios.create({ baseURL: validConfig.baseURL }) 30 | const apisauce = create({ axiosInstance: customAxiosInstance, headers: validConfig.headers }) 31 | const { axiosInstance } = apisauce 32 | t.is(axiosInstance.defaults.timeout, 0) 33 | t.is(axiosInstance.defaults.baseURL, validConfig.baseURL) 34 | t.deepEqual(apisauce.headers, Object.assign({}, DEFAULT_HEADERS, validConfig.headers)) 35 | }) 36 | -------------------------------------------------------------------------------- /test/data.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { create } from '../lib/apisauce' 3 | import createServer from './_server' 4 | import getFreePort from './_getFreePort' 5 | 6 | const MOCK = { a: { b: [1, 2, 3] } } 7 | let port 8 | let server = null 9 | test.before(async t => { 10 | port = await getFreePort() 11 | server = await createServer(port, MOCK) 12 | }) 13 | 14 | test.after.always('cleanup', t => { 15 | server.close() 16 | }) 17 | 18 | test('has valid data with a 200', t => { 19 | const x = create({ baseURL: `http://localhost:${port}` }) 20 | return x.get('/number/200', { a: 'b' }).then(response => { 21 | t.is(response.status, 200) 22 | t.deepEqual(response.data, MOCK) 23 | }) 24 | }) 25 | 26 | test('has valid data with a 400s', t => { 27 | const x = create({ baseURL: `http://localhost:${port}` }) 28 | return x.get('/number/404').then(response => { 29 | t.is(response.status, 404) 30 | t.deepEqual(response.data, MOCK) 31 | }) 32 | }) 33 | 34 | test('has valid data with a 500s', t => { 35 | const x = create({ baseURL: `http://localhost:${port}` }) 36 | return x.get('/number/500').then(response => { 37 | t.is(response.status, 500) 38 | t.deepEqual(response.data, MOCK) 39 | }) 40 | }) 41 | 42 | test('Falsy data is preserved', t => { 43 | const x = create({ baseURL: `http://localhost:${port}` }) 44 | return x.get('/falsy').then(response => { 45 | t.is(response.data, false) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /test/headers.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { create } from '../lib/apisauce' 3 | import createServer from './_server' 4 | import getFreePort from './_getFreePort' 5 | 6 | const MOCK = { a: { b: [1, 2, 3] } } 7 | let port 8 | let server = null 9 | test.before(async t => { 10 | port = await getFreePort() 11 | server = await createServer(port, MOCK) 12 | }) 13 | 14 | test.after('cleanup', t => { 15 | server.close() 16 | }) 17 | 18 | test('jumps the wire with the right headers', async t => { 19 | const api = create({ 20 | baseURL: `http://localhost:${port}`, 21 | headers: { 'X-Testing': 'hello' }, 22 | }) 23 | api.setHeaders({ 'X-Testing': 'foo', steve: 'hey' }) 24 | const response = await api.get('/number/200', { a: 'b' }) 25 | t.is(response.config.headers['X-Testing'], 'foo') 26 | t.is(response.config.headers['steve'], 'hey') 27 | 28 | // then change one of them 29 | api.setHeader('steve', 'thx') 30 | const response2 = await api.get('/number/200', {}) 31 | t.is(response2.config.headers['steve'], 'thx') 32 | 33 | // then remove one of them 34 | api.deleteHeader('steve') 35 | const response3 = await api.get('/number/200', {}) 36 | t.is(response3.config.headers['steve'], undefined) 37 | }) 38 | -------------------------------------------------------------------------------- /test/monitor.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { create } from '../lib/apisauce' 3 | import createServer from './_server' 4 | import getFreePort from './_getFreePort' 5 | 6 | const MOCK = { a: { b: [1, 2, 3] } } 7 | let port 8 | let server = null 9 | test.before(async t => { 10 | port = await getFreePort() 11 | server = await createServer(port, MOCK) 12 | }) 13 | 14 | test.after('cleanup', t => { 15 | server.close() 16 | }) 17 | 18 | test('attaches a monitor', t => { 19 | const api = create({ baseURL: `http://localhost:${port}` }) 20 | t.truthy(api.addMonitor) 21 | t.truthy(api.monitors) 22 | t.is(api.monitors.length, 0) 23 | api.addMonitor(x => x) 24 | t.is(api.monitors.length, 1) 25 | }) 26 | 27 | test('fires our monitor function', t => { 28 | let a = 0 29 | let b = 0 30 | const x = create({ baseURL: `http://localhost:${port}` }) 31 | x.addMonitor(response => { 32 | a += 1 33 | }) 34 | x.addMonitor(response => { 35 | b = response.status 36 | }) 37 | t.is(a, 0) 38 | return x.get('/number/201').then(response => { 39 | t.is(response.status, 201) 40 | t.is(a, 1) 41 | t.is(b, 201) 42 | }) 43 | }) 44 | 45 | test('ignores exceptions raised inside monitors', t => { 46 | let a = 0 47 | let b = 0 48 | const x = create({ baseURL: `http://localhost:${port}` }) 49 | x.addMonitor(response => { 50 | a += 1 51 | }) 52 | x.addMonitor(response => { 53 | this.recklessDisregardForAllThingsJust(true) 54 | }) 55 | x.addMonitor(response => { 56 | b = response.status 57 | }) 58 | t.is(a, 0) 59 | return x.get('/number/201').then(response => { 60 | t.is(response.status, 201) 61 | t.is(a, 1) 62 | t.is(b, 201) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /test/no-server.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { create, CONNECTION_ERROR } from '../lib/apisauce' 3 | import getFreePort from './_getFreePort' 4 | 5 | let port 6 | test.before(async t => { 7 | port = await getFreePort() 8 | }) 9 | 10 | test('has a response despite no server', t => { 11 | const x = create({ baseURL: `http://localhost:${port}` }) 12 | return x.get('/number/200', { a: 'b' }).then(response => { 13 | t.is(response.status, null) 14 | t.is(response.problem, CONNECTION_ERROR) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /test/params.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { create, NONE } from '../lib/apisauce' 3 | import createServer from './_server' 4 | import getFreePort from './_getFreePort' 5 | 6 | let port 7 | let server = null 8 | test.before(async t => { 9 | port = await getFreePort() 10 | server = await createServer(port) 11 | }) 12 | 13 | test.after('cleanup', t => { 14 | server.close() 15 | }) 16 | 17 | test('GET supports params', t => { 18 | const x = create({ baseURL: `http://localhost:${port}` }) 19 | return x.get('/echo', { q: 'hello' }).then(response => { 20 | t.is(response.problem, NONE) 21 | t.deepEqual(response.data, { echo: 'hello' }) 22 | }) 23 | }) 24 | 25 | test('POST supports params', t => { 26 | const x = create({ baseURL: `http://localhost:${port}` }) 27 | return x.post('/echo', null, { params: { q: 'hello' } }).then(response => { 28 | t.is(response.problem, NONE) 29 | t.deepEqual(response.data, { echo: 'hello' }) 30 | }) 31 | }) 32 | 33 | test('PATCH supports params', t => { 34 | const x = create({ baseURL: `http://localhost:${port}` }) 35 | return x.patch('/echo', null, { params: { q: 'hello' } }).then(response => { 36 | t.is(response.problem, NONE) 37 | t.deepEqual(response.data, { echo: 'hello' }) 38 | }) 39 | }) 40 | 41 | test('PUT supports params', t => { 42 | const x = create({ baseURL: `http://localhost:${port}` }) 43 | return x.put('/echo', null, { params: { q: 'hello' } }).then(response => { 44 | t.is(response.problem, NONE) 45 | t.deepEqual(response.data, { echo: 'hello' }) 46 | }) 47 | }) 48 | 49 | test('DELETE supports params', t => { 50 | const x = create({ baseURL: `http://localhost:${port}` }) 51 | return x.delete('/echo', { q: 'hello' }).then(response => { 52 | t.is(response.problem, NONE) 53 | t.deepEqual(response.data, { echo: 'hello' }) 54 | }) 55 | }) 56 | 57 | test('LINK supports params', t => { 58 | const x = create({ baseURL: `http://localhost:${port}` }) 59 | return x.link('/echo', { q: 'hello' }).then(response => { 60 | t.is(response.problem, NONE) 61 | t.deepEqual(response.data, { echo: 'hello' }) 62 | }) 63 | }) 64 | 65 | test('UNLINK supports params', t => { 66 | const x = create({ baseURL: `http://localhost:${port}` }) 67 | return x.unlink('/echo', { q: 'hello' }).then(response => { 68 | t.is(response.problem, NONE) 69 | t.deepEqual(response.data, { echo: 'hello' }) 70 | }) 71 | }) 72 | 73 | test('Empty params are supported', t => { 74 | const x = create({ baseURL: `http://localhost:${port}` }) 75 | return x.get('/echo', {}).then(response => { 76 | t.is(response.problem, NONE) 77 | t.deepEqual(response.data, { echo: '' }) 78 | }) 79 | }) 80 | 81 | test('Null params are supported', t => { 82 | const x = create({ baseURL: `http://localhost:${port}` }) 83 | return x.get('/echo', null).then(response => { 84 | t.is(response.problem, NONE) 85 | t.deepEqual(response.data, { echo: '' }) 86 | }) 87 | }) 88 | 89 | test('Undefined params are supported', t => { 90 | const x = create({ baseURL: `http://localhost:${port}` }) 91 | return x.get('/echo').then(response => { 92 | t.is(response.problem, NONE) 93 | t.deepEqual(response.data, { echo: '' }) 94 | }) 95 | }) 96 | 97 | test('Null parameters should be null', t => { 98 | const x = create({ baseURL: `http://localhost:${port}` }) 99 | return x.get('/echo', { q: null }).then(response => { 100 | t.is(response.problem, NONE) 101 | t.deepEqual(response.data, { echo: '' }) 102 | }) 103 | }) 104 | 105 | test('Empty parameters should be empty', t => { 106 | const x = create({ baseURL: `http://localhost:${port}` }) 107 | return x.get('/echo', { q: '' }).then(response => { 108 | t.is(response.problem, NONE) 109 | t.deepEqual(response.data, { echo: '' }) 110 | }) 111 | }) 112 | -------------------------------------------------------------------------------- /test/post-data.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { create } from '../lib/apisauce' 3 | import createServer from './_server' 4 | import getFreePort from './_getFreePort' 5 | 6 | const MOCK = { a: { b: [1, 2, 3] } } 7 | let port 8 | let server = null 9 | test.before(async t => { 10 | port = await getFreePort() 11 | server = await createServer(port, MOCK) 12 | }) 13 | 14 | test.after('cleanup', t => { 15 | server.close() 16 | }) 17 | 18 | test('POST has proper data', t => { 19 | const x = create({ baseURL: `http://localhost:${port}` }) 20 | return x.post('/post', MOCK).then(response => { 21 | t.is(response.status, 200) 22 | t.deepEqual(response.data, { got: MOCK }) 23 | }) 24 | }) 25 | 26 | test('PATCH has proper data', t => { 27 | const x = create({ baseURL: `http://localhost:${port}` }) 28 | return x.patch('/post', MOCK).then(response => { 29 | t.is(response.status, 200) 30 | t.deepEqual(response.data, { got: MOCK }) 31 | }) 32 | }) 33 | 34 | test('PUT has proper data', t => { 35 | const x = create({ baseURL: `http://localhost:${port}` }) 36 | return x.put('/post', MOCK).then(response => { 37 | t.is(response.status, 200) 38 | t.deepEqual(response.data, { got: MOCK }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /test/request-transform.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { create } from '../lib/apisauce' 3 | import createServer from './_server' 4 | import getFreePort from './_getFreePort' 5 | 6 | const MOCK = { a: { b: [1, 2, 3] } } 7 | let port 8 | let server = null 9 | test.before(async t => { 10 | port = await getFreePort() 11 | server = await createServer(port, MOCK) 12 | }) 13 | 14 | test.after('cleanup', t => { 15 | server.close() 16 | }) 17 | 18 | test('attaches a request transform', t => { 19 | const api = create({ baseURL: `http://localhost:${port}` }) 20 | t.truthy(api.addRequestTransform) 21 | t.truthy(api.requestTransforms) 22 | t.is(api.requestTransforms.length, 0) 23 | api.addRequestTransform(request => request) 24 | t.is(api.requestTransforms.length, 1) 25 | }) 26 | 27 | test('alters the request data', t => { 28 | const x = create({ baseURL: `http://localhost:${port}` }) 29 | let count = 0 30 | x.addRequestTransform(({ data, url, method }) => { 31 | data.a = 'hi' 32 | count++ 33 | }) 34 | t.is(count, 0) 35 | return x.post('/post', MOCK).then(response => { 36 | t.is(response.status, 200) 37 | t.is(count, 1) 38 | t.deepEqual(response.data, { got: { a: 'hi' } }) 39 | }) 40 | }) 41 | 42 | test('survives empty PUTs', t => { 43 | const x = create({ baseURL: `http://localhost:${port}` }) 44 | let count = 0 45 | x.addRequestTransform(() => { 46 | count++ 47 | }) 48 | t.is(count, 0) 49 | return x.put('/post', {}).then(response => { 50 | t.is(response.status, 200) 51 | t.is(count, 1) 52 | }) 53 | }) 54 | 55 | test('fires for gets', t => { 56 | const x = create({ baseURL: `http://localhost:${port}` }) 57 | let count = 0 58 | x.addRequestTransform(({ data, url, method }) => { 59 | count++ 60 | }) 61 | t.is(count, 0) 62 | return x.get('/number/201').then(response => { 63 | t.is(response.status, 201) 64 | t.is(count, 1) 65 | t.deepEqual(response.data, MOCK) 66 | }) 67 | }) 68 | 69 | test('url can be changed', t => { 70 | const x = create({ baseURL: `http://localhost:${port}` }) 71 | x.addRequestTransform(request => { 72 | request.url = request.url.replace('/201', '/200') 73 | }) 74 | return x.get('/number/201', { x: 1 }).then(response => { 75 | t.is(response.status, 200) 76 | }) 77 | }) 78 | 79 | test('params can be added, edited, and deleted', t => { 80 | const x = create({ baseURL: `http://localhost:${port}` }) 81 | x.addRequestTransform(request => { 82 | request.params.x = 2 83 | request.params.y = 1 84 | delete request.params.z 85 | }) 86 | return x.get('/number/200', { x: 1, z: 4 }).then(response => { 87 | t.is(response.status, 200) 88 | t.is(response.config.params.x, 2) 89 | t.is(response.config.params.y, 1) 90 | t.falsy(response.config.params.z) 91 | }) 92 | }) 93 | 94 | test('headers can be created', t => { 95 | const x = create({ baseURL: `http://localhost:${port}` }) 96 | x.addRequestTransform(request => { 97 | t.falsy(request.headers['X-APISAUCE']) 98 | request.headers['X-APISAUCE'] = 'new' 99 | }) 100 | return x.get('/number/201', { x: 1 }).then(response => { 101 | t.is(response.status, 201) 102 | t.is(response.config.headers['X-APISAUCE'], 'new') 103 | }) 104 | }) 105 | 106 | test('headers from creation time can be changed', t => { 107 | const x = create({ 108 | baseURL: `http://localhost:${port}`, 109 | headers: { 'X-APISAUCE': 'hello' }, 110 | }) 111 | x.addRequestTransform(request => { 112 | t.is(request.headers['X-APISAUCE'], 'hello') 113 | request.headers['X-APISAUCE'] = 'change' 114 | }) 115 | return x.get('/number/201', { x: 1 }).then(response => { 116 | t.is(response.status, 201) 117 | t.is(response.config.headers['X-APISAUCE'], 'change') 118 | }) 119 | }) 120 | 121 | test('headers can be deleted', t => { 122 | const x = create({ 123 | baseURL: `http://localhost:${port}`, 124 | headers: { 'X-APISAUCE': 'omg' }, 125 | }) 126 | x.addRequestTransform(request => { 127 | t.is(request.headers['X-APISAUCE'], 'omg') 128 | delete request.headers['X-APISAUCE'] 129 | }) 130 | return x.get('/number/201', { x: 1 }).then(response => { 131 | t.is(response.status, 201) 132 | t.falsy(response.config.headers['X-APISAUCE']) 133 | }) 134 | }) 135 | -------------------------------------------------------------------------------- /test/response-transform.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { create } from '../lib/apisauce' 3 | import createServer from './_server' 4 | import getFreePort from './_getFreePort' 5 | 6 | const MOCK = { a: { b: [1, 2, 3] } } 7 | let port 8 | let server = null 9 | test.before(async t => { 10 | port = await getFreePort() 11 | server = await createServer(port, MOCK) 12 | }) 13 | 14 | test.after('cleanup', t => { 15 | server.close() 16 | }) 17 | 18 | test('attaches a response transform', t => { 19 | const api = create({ baseURL: `http://localhost:${port}` }) 20 | t.truthy(api.addResponseTransform) 21 | t.truthy(api.responseTransforms) 22 | t.is(api.responseTransforms.length, 0) 23 | api.addResponseTransform(response => response) 24 | t.is(api.responseTransforms.length, 1) 25 | }) 26 | 27 | test('alters the response', t => { 28 | const x = create({ baseURL: `http://localhost:${port}` }) 29 | let count = 0 30 | x.addResponseTransform(({ data }) => { 31 | count++ 32 | data.a = 'hi' 33 | }) 34 | t.is(count, 0) 35 | return x.get('/number/201').then(response => { 36 | t.is(response.status, 201) 37 | t.is(count, 1) 38 | t.deepEqual(response.data.a, 'hi') 39 | }) 40 | }) 41 | 42 | test('swap out data on response', t => { 43 | const x = create({ baseURL: `http://localhost:${port}` }) 44 | let count = 0 45 | x.addResponseTransform(response => { 46 | count++ 47 | response.status = 222 48 | response.data = { a: response.data.a.b.reverse() } 49 | }) 50 | // t.is(count, 0) 51 | return x.get('/number/201').then(response => { 52 | t.is(response.status, 222) 53 | t.is(count, 1) 54 | t.deepEqual(response.data.a, [3, 2, 1]) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /test/set-base-url.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { create } from '../lib/apisauce' 3 | import createServer from './_server' 4 | import getFreePort from './_getFreePort' 5 | 6 | const MOCK = { a: { b: [1, 2, 3] } } 7 | let port 8 | let server = null 9 | test.before(async t => { 10 | port = await getFreePort() 11 | server = await createServer(port, MOCK) 12 | }) 13 | 14 | test.after('cleanup', t => { 15 | server.close() 16 | }) 17 | 18 | test('changes the headers', async t => { 19 | const api = create({ 20 | baseURL: `http://localhost:${port}`, 21 | headers: { 'X-Testing': 'hello' }, 22 | }) 23 | const response1 = await api.get('/number/200') 24 | t.deepEqual(response1.data, MOCK) 25 | 26 | // change the url 27 | const nextUrl = `http://127.0.0.1:${port}` 28 | api.setBaseURL(nextUrl) 29 | t.is(api.getBaseURL(), nextUrl) 30 | const response2 = await api.get('/number/200') 31 | t.deepEqual(response2.data, MOCK) 32 | 33 | // now close the server 34 | server.close() 35 | 36 | // and try connecting back to the original one 37 | api.setBaseURL(`http://localhost:${port}`) 38 | const response3 = await api.get('/number/200') 39 | t.is(response3.problem, 'CONNECTION_ERROR') 40 | }) 41 | -------------------------------------------------------------------------------- /test/speed.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { create } from '../lib/apisauce' 3 | import createServer from './_server' 4 | import getFreePort from './_getFreePort' 5 | 6 | const MOCK = { a: { b: [1, 2, 3] } } 7 | let port 8 | let server = null 9 | test.before(async t => { 10 | port = await getFreePort() 11 | server = await createServer(port, MOCK) 12 | }) 13 | 14 | test.after('cleanup', t => { 15 | server.close() 16 | }) 17 | 18 | test('has a duration node', async t => { 19 | const x = create({ baseURL: `http://localhost:${port}` }) 20 | const response = await x.get(`/sleep/150`) 21 | t.is(response.status, 200) 22 | t.truthy(response.duration) 23 | t.truthy(response.duration >= 150) 24 | // t.truthy(response.duration <= 1000) // fragile 25 | }) 26 | -------------------------------------------------------------------------------- /test/status.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { create, NONE, CLIENT_ERROR, SERVER_ERROR } from '../lib/apisauce' 3 | import createServer from './_server' 4 | import getFreePort from './_getFreePort' 5 | 6 | let port 7 | let server = null 8 | test.before(async t => { 9 | port = await getFreePort() 10 | server = await createServer(port) 11 | }) 12 | 13 | test.after('cleanup', t => { 14 | server.close() 15 | }) 16 | 17 | test('reads the status code for 200s', t => { 18 | const x = create({ baseURL: `http://localhost:${port}` }) 19 | return x.get('/number/201').then(response => { 20 | t.is(response.status, 201) 21 | t.is(response.problem, NONE) 22 | }) 23 | }) 24 | 25 | test('reads the status code for 400s', t => { 26 | const x = create({ baseURL: `http://localhost:${port}` }) 27 | return x.get('/number/401').then(response => { 28 | t.is(response.status, 401) 29 | t.is(response.problem, CLIENT_ERROR) 30 | }) 31 | }) 32 | 33 | test('reads the status code for 500s', t => { 34 | const x = create({ baseURL: `http://localhost:${port}` }) 35 | return x.get('/number/501').then(response => { 36 | t.is(response.status, 501) 37 | t.is(response.problem, SERVER_ERROR) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /test/timeout.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { create, TIMEOUT_ERROR } from '../lib/apisauce' 3 | import createServer from './_server' 4 | import getFreePort from './_getFreePort' 5 | 6 | let port 7 | let server = null 8 | test.before(async t => { 9 | port = await getFreePort() 10 | server = await createServer(port) 11 | }) 12 | 13 | test.after('cleanup', t => { 14 | server.close() 15 | }) 16 | 17 | test('times out', t => { 18 | const x = create({ baseURL: `http://localhost:${port}`, timeout: 100 }) 19 | return x.get('/sleep/150').then(response => { 20 | t.falsy(response.ok) 21 | t.is(response.problem, TIMEOUT_ERROR) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /test/verbs.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { create } from '../lib/apisauce' 3 | import createServer from './_server' 4 | import getFreePort from './_getFreePort' 5 | 6 | let port 7 | let server = null 8 | test.before(async t => { 9 | port = await getFreePort() 10 | server = await createServer(port) 11 | }) 12 | 13 | test.after('cleanup', t => { 14 | server.close() 15 | }) 16 | 17 | test('supports all verbs', t => { 18 | const x = create({ baseURL: `http://localhost:${port}` }) 19 | t.truthy(x.get) 20 | t.truthy(x.post) 21 | t.truthy(x.patch) 22 | t.truthy(x.put) 23 | t.truthy(x.head) 24 | t.truthy(x.delete) 25 | t.truthy(x.link) 26 | t.truthy(x.unlink) 27 | }) 28 | 29 | test('can make a get', t => { 30 | const x = create({ baseURL: `http://localhost:${port}` }) 31 | return x.get('/ok').then(response => { 32 | t.truthy(response.ok) 33 | t.is(response.config.method, 'get') 34 | }) 35 | }) 36 | 37 | test('can make a post', t => { 38 | const x = create({ baseURL: `http://localhost:${port}` }) 39 | return x.post('/ok').then(response => { 40 | t.truthy(response.ok) 41 | t.is(response.config.method, 'post') 42 | }) 43 | }) 44 | 45 | test('can make a patch', t => { 46 | const x = create({ baseURL: `http://localhost:${port}` }) 47 | return x.patch('/ok').then(response => { 48 | t.truthy(response.ok) 49 | t.is(response.config.method, 'patch') 50 | }) 51 | }) 52 | 53 | test('can make a put', t => { 54 | const x = create({ baseURL: `http://localhost:${port}` }) 55 | return x.put('/ok').then(response => { 56 | t.truthy(response.ok) 57 | t.is(response.config.method, 'put') 58 | }) 59 | }) 60 | 61 | test('can make a delete', t => { 62 | const x = create({ baseURL: `http://localhost:${port}` }) 63 | return x.delete('/ok').then(response => { 64 | t.truthy(response.ok) 65 | t.is(response.config.method, 'delete') 66 | }) 67 | }) 68 | 69 | test('can make a head', t => { 70 | const x = create({ baseURL: `http://localhost:${port}` }) 71 | return x.head('/ok').then(response => { 72 | t.truthy(response.ok) 73 | t.is(response.config.method, 'head') 74 | }) 75 | }) 76 | 77 | test('can make a link', t => { 78 | const x = create({ baseURL: `http://localhost:${port}` }) 79 | return x.link('/ok').then(response => { 80 | t.truthy(response.ok) 81 | t.is(response.config.method, 'link') 82 | }) 83 | }) 84 | 85 | test('can make a unlink', t => { 86 | const x = create({ baseURL: `http://localhost:${port}` }) 87 | return x.unlink('/ok').then(response => { 88 | t.truthy(response.ok) 89 | t.is(response.config.method, 'unlink') 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es5", 5 | "module": "es2015", 6 | "strict": false, 7 | "lib": ["dom", "es2015"] 8 | }, 9 | "include": ["lib"] 10 | } 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-standard", "tslint-config-prettier"], 3 | "rules": { 4 | "strict-type-predicates": false 5 | } 6 | } 7 | --------------------------------------------------------------------------------