fch 0.4.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/fetch.js CHANGED
@@ -1,34 +1,25 @@
1
- import swear from './node_modules/swear/swear.js';
2
-
3
- if (typeof require !== 'undefined') {
4
- require('isomorphic-fetch');
5
- }
1
+ import swear from "swear";
6
2
 
7
3
  // To avoid making parallel requests to the same url if one is ongoing
8
4
  const ongoing = new Map();
9
5
 
10
6
  // Plain-ish object
11
- const hasPlainBody = options => {
12
- if (options.headers['content-type']) return;
13
- if (typeof options.body !== 'object') return;
7
+ const hasPlainBody = (options) => {
8
+ if (options.headers["content-type"]) return;
9
+ if (typeof options.body !== "object") return;
14
10
  if (options.body instanceof FormData) return;
15
11
  return true;
16
12
  };
17
13
 
18
- const fch = (url, options = {}) => {
19
-
20
- options = {
21
- method: 'get',
22
- headers: {},
23
- ...(typeof options === 'object' ? options : {})
24
- };
25
-
26
- // GET requests should not have race conditions
27
- if (options.method.toLowerCase() === 'get') {
28
- if (ongoing.get(url)) return ongoing.get(url);
29
- }
14
+ const createUrl = (path, base) => {
15
+ if (!base) return path;
16
+ const url = new URL(path, base);
17
+ return url.href;
18
+ };
30
19
 
31
- const headers = options.headers;
20
+ const createHeaders = (user, base) => {
21
+ // User-set headers overwrite the base headers
22
+ const headers = { ...base, ...user };
32
23
 
33
24
  // Make the headers lowercase
34
25
  for (let key in headers) {
@@ -37,44 +28,144 @@ const fch = (url, options = {}) => {
37
28
  headers[key.toLowerCase()] = value;
38
29
  }
39
30
 
40
- // JSON-encode plain objects
41
- if (hasPlainBody(options)) {
42
- options.body = JSON.stringify(options.body);
43
- headers['content-type'] = 'application/json; charset=utf-8';
44
- }
31
+ return headers;
32
+ };
45
33
 
46
- ongoing.set(url, swear(fetch(url, { ...options, headers }).then(res => {
34
+ const createFetch = (request, { after, dedupe, error, output }) => {
35
+ return fetch(request.url, request).then(async (res) => {
47
36
  // No longer ongoing at this point
48
- ongoing.delete(url);
37
+ if (dedupe) dedupe.clear();
38
+
39
+ // Need to manually create it to set some things like the proper response
40
+ let response = {
41
+ status: res.status,
42
+ statusText: res.statusText,
43
+ headers: {},
44
+ };
45
+ for (let key of res.headers.keys()) {
46
+ response.headers[key.toLowerCase()] = res.headers.get(key);
47
+ }
49
48
 
50
- // Everything is good, just keep going
49
+ // Oops, throw it
51
50
  if (!res.ok) {
52
- // Oops, throw it
53
- const error = new Error(res.statusText);
54
- error.response = res;
55
- return Promise.reject(error);
51
+ const err = new Error(res.statusText);
52
+ err.response = response;
53
+ return error(err);
56
54
  }
57
55
 
58
- const mem = new Map();
59
- return new Proxy(res, { get: (target, key) => {
60
- if (['then', 'catch', 'finally'].includes(key)) return res[key];
61
- return () => {
62
- if (!mem.get(key)) {
63
- mem.set(key, target[key]());
64
- }
65
- return mem.get(key);
66
- };
67
- }});
68
- })));
69
-
70
- return ongoing.get(url);
56
+ // Automatically parse the response
57
+ const isJson = res.headers.get("content-type").includes("application/json");
58
+ response.body = await (isJson ? res.json() : res.text());
59
+
60
+ // Hijack the response and modify it
61
+ if (after) {
62
+ response = after(response);
63
+ }
64
+
65
+ if (output === "body") {
66
+ return response.body;
67
+ } else {
68
+ return response;
69
+ }
70
+ });
71
71
  };
72
72
 
73
- fch.head = (url, options = {}) => fch(url, { ...options, method: 'head' });
74
- fch.get = (url, options = {}) => fch(url, { ...options, method: 'get' });
75
- fch.post = (url, options = {}) => fch(url, { ...options, method: 'post' });
76
- fch.patch = (url, options = {}) => fch(url, { ...options, method: 'patch' });
77
- fch.put = (url, options = {}) => fch(url, { ...options, method: 'put' });
78
- fch.del = (url, options = {}) => fch(url, { ...options, method: 'delete' });
73
+ const fch = swear((url, options = {}) => {
74
+ // Second parameter always has to be an object, even when it defaults
75
+ if (typeof options !== "object") options = {};
76
+
77
+ // Accept either fch(options) or fch(url, options)
78
+ options = typeof url === "string" ? { url, ...options } : url;
79
+
80
+ // Absolute URL if possible; Default method; merge the default headers
81
+ options.url = createUrl(options.url, fch.baseUrl);
82
+ options.method = (options.method ?? fch.method).toLowerCase();
83
+ options.headers = createHeaders(options.headers, fch.headers);
84
+
85
+ let {
86
+ dedupe = fch.dedupe,
87
+ output = fch.output,
88
+
89
+ before = fch.before,
90
+ after = fch.after,
91
+ error = fch.error,
92
+
93
+ ...request
94
+ } = options; // Local option OR global value (including defaults)
95
+
96
+ if (request.method !== "get") {
97
+ dedupe = false;
98
+ }
99
+ if (dedupe) {
100
+ dedupe = {
101
+ save: (prom) => {
102
+ ongoing.set(request.url, prom);
103
+ return prom;
104
+ },
105
+ get: () => ongoing.get(request.url),
106
+ clear: () => ongoing.delete(request.url),
107
+ };
108
+ }
109
+
110
+ if (!["body", "response"].includes(output)) {
111
+ const msg = `options.output needs to be either "body" (default) or "response", not "${output}"`;
112
+ throw new Error(msg);
113
+ }
114
+
115
+ // JSON-encode plain objects
116
+ if (hasPlainBody(request)) {
117
+ request.body = JSON.stringify(request.body);
118
+ request.headers["content-type"] = "application/json; charset=utf-8";
119
+ }
120
+
121
+ // Hijack the requeset and modify it
122
+ if (before) {
123
+ request = before(request);
124
+ }
125
+
126
+ // It should be cached
127
+ if (dedupe) {
128
+ // It's already cached! Just return it
129
+ if (dedupe.get()) return dedupe.get();
130
+
131
+ // Otherwise, save it in the cache and return the promise
132
+ return dedupe.save(createFetch(request, { dedupe, output, error, after }));
133
+ } else {
134
+ // PUT, POST, etc should never dedupe and just return the plain request
135
+ return createFetch(request, { output, error, after });
136
+ }
137
+ });
138
+
139
+ // Default values
140
+ fch.method = "get";
141
+ fch.headers = {};
142
+
143
+ // Default options
144
+ fch.dedupe = true;
145
+ fch.output = "body";
146
+
147
+ // Interceptors
148
+ fch.before = (request) => request;
149
+ fch.after = (response) => response;
150
+ fch.error = (error) => Promise.reject(error);
151
+
152
+ const request = (url, opts = {}) => fch(url, { ...opts });
153
+ const get = (url, opts = {}) => fch(url, { ...opts });
154
+ const head = (url, opts = {}) => fch(url, { ...opts, method: "head" });
155
+ const post = (url, opts = {}) => fch(url, { ...opts, method: "post" });
156
+ const patch = (url, opts = {}) => fch(url, { ...opts, method: "patch" });
157
+ const put = (url, opts = {}) => fch(url, { ...opts, method: "put" });
158
+ const del = (url, opts = {}) => fch(url, { ...opts, method: "delete" });
159
+
160
+ fch.request = request;
161
+ fch.get = get;
162
+ fch.head = head;
163
+ fch.post = post;
164
+ fch.patch = patch;
165
+ fch.put = put;
166
+ fch.del = del;
167
+
168
+ fch.swear = swear;
79
169
 
80
170
  export default fch;
171
+ export { request, get, head, post, patch, put, del, swear };
package/package.json CHANGED
@@ -1,36 +1,41 @@
1
1
  {
2
2
  "name": "fch",
3
- "version": "0.4.0",
4
- "description": "Fetch interface with get queue, credentials, reject on http error and better promises",
5
- "main": "fetch.min.js",
6
- "scripts": {
7
- "start": "npm run watch # Start ~= Start dev",
8
- "build": "rollup fetch.js --name fch --output.format umd | uglifyjs -o fetch.min.js",
9
- "test": "npm run build && jest --coverage --detectOpenHandles",
10
- "watch": "nodemon --exec \"npm run build && npm test && npm run gzip\" --watch src --watch test --watch webpack.config.js --watch package.json",
11
- "gzip": "gzip -c fetch.min.js | wc -c && echo 'bytes' # Only for Unix"
12
- },
13
- "keywords": [],
3
+ "version": "2.0.1",
4
+ "description": "Fetch interface with better promises, deduplication, defaults, etc.",
5
+ "homepage": "https://github.com/franciscop/fetch",
6
+ "repository": "https://github.com/franciscop/fetch.git",
7
+ "bugs": "https://github.com/franciscop/fetch/issues",
8
+ "funding": "https://www.paypal.me/franciscopresencia/19",
14
9
  "author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
15
10
  "license": "MIT",
16
- "repository": {
17
- "type": "git",
18
- "url": "git+https://github.com/franciscop/fetch.git"
11
+ "scripts": {
12
+ "start": "npm run watch # Start ~= Start dev",
13
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --detectOpenHandles",
14
+ "watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --coverage --detectOpenHandles"
19
15
  },
20
- "bugs": {
21
- "url": "https://github.com/franciscop/fetch/issues"
16
+ "keywords": [
17
+ "fetch",
18
+ "axios",
19
+ "http",
20
+ "https",
21
+ "async",
22
+ "ajax"
23
+ ],
24
+ "main": "fetch.js",
25
+ "files": [],
26
+ "type": "module",
27
+ "engines": {
28
+ "node": ">=18.0.0"
22
29
  },
23
30
  "dependencies": {
24
- "isomorphic-fetch": "^2.2.1",
25
- "swear": "^1.0.0"
31
+ "swear": "^1.1.0"
26
32
  },
27
33
  "devDependencies": {
28
- "babel-core": "^6.26.0",
29
- "babel-jest": "^21.2.0",
30
- "babel-preset-env": "^1.7.0",
31
- "jest": "^23.5.0",
32
- "jest-fetch-mock": "^1.6.6",
33
- "rollup": "^1.1.2",
34
- "uglify-es": "^3.3.9"
34
+ "jest": "^28.0.1",
35
+ "jest-fetch-mock": "^3.0.3"
36
+ },
37
+ "jest": {
38
+ "testEnvironment": "jest-environment-node",
39
+ "transform": {}
35
40
  }
36
41
  }
package/readme.md CHANGED
@@ -1,81 +1,377 @@
1
- # Fetch [![npm install fch](https://img.shields.io/badge/npm%20install-fch-blue.svg)](https://www.npmjs.com/package/fch) [![gzip size](https://img.badgesize.io/franciscop/fetch/master/fetch.min.js.svg?compression=gzip)](https://github.com/franciscop/fetch/blob/master/fetch.min.js)
1
+ # Fch [![npm install fch](https://img.shields.io/badge/npm%20install-fch-blue.svg)](https://www.npmjs.com/package/fch) [![gzip size](https://img.badgesize.io/franciscop/fetch/master/fetch.js.svg?compression=gzip)](https://github.com/franciscop/fetch/blob/master/fetch.js)
2
2
 
3
- `fetch()` greatly improved:
3
+ A library to make API calls easier. Similar to Axios, but tiny size and simpler API:
4
4
 
5
5
  ```js
6
- import fch from 'fch';
6
+ import api from "fch";
7
+ const mew = await api("https://pokeapi.co/pokemon/150");
8
+ console.log(mew);
9
+ ```
10
+
11
+ `fch` is a better `fetch()`:
12
+
13
+ - Automatically `JSON.stringify()` and `Content-Type: 'application/json'` for plain objects.
14
+ - Automatically parse server response as json if it includes the headers, or text otherwise.
15
+ - Works the same way in Node.js and the browser.
16
+ - Await/Async Promise interface works as you know and love.
17
+ - Better error handling. `>= 400 and <= 100` will _reject_ the promise with an error instance.
18
+ - Advanced [Promise interface](https://www.npmjs.com/swear) for better scripting.
19
+ - Easily define shared options straight on the root `fetch.baseUrl = "https://...";`.
20
+ - Interceptors: `before` (the request), `after` (the response) and `error` (it fails).
21
+ - Deduplicates parallel GET requests.
22
+ - Configurable to return either just the body, or the full response.
23
+ - [TODO]: cache engine with "highs" and "lows", great for scrapping
24
+ - [TODO]: rate-limiting of requests (N-second, or N-parallel), great for scrapping
25
+
26
+ These are the available options and their defaults:
27
+
28
+ - `api.baseUrl = null;`: Set an API endpoint
29
+ - `api.method = 'get';`: Default method to use for api()
30
+ - `api.headers = {};`: Merged with the headers on a per-request basis
31
+ - `api.dedupe = true;`: Avoid parallel GET requests to the same path
32
+ - `api.output = 'body';`: Return the body; use 'response' for the full response
33
+ - `api.before = req => req;`: Interceptor executed before sending the request
34
+ - `api.after = res => res;`: Handle the responses before returning them
35
+ - `api.error = err => Promise.reject(err);`: handle any error thrown by fch
36
+ - `api(url, { method, body, headers, ... })`
37
+ - `api.get(url, { headers, ... });`: helper for convenience
38
+ - `api.head(url, { headers, ... });`: helper for convenience
39
+ - `api.post(url, { body, headers, ... });`: helper for convenience
40
+ - `api.patch(url, { body, headers, ... });`: helper for convenience
41
+ - `api.put(url, { body, headers, ... });`: helper for convenience
42
+ - `api.del(url, { body, headers, ... });`: helper for convenience
43
+
44
+ ## Getting Started
45
+
46
+ Install it in your project:
7
47
 
8
- // Example data: { "name": "Francisco" }
9
- const url = 'https://api.jsonbin.io/b/5bc69ae7716f9364f8c58651';
48
+ ```bash
49
+ npm install fch
50
+ ```
10
51
 
11
- (async () => {
12
- // Using the Swear interface
13
- const name = await fch(url).json().name;
14
- console.log(name); // "Francisco"
52
+ Then import it to be able to use it in your code:
15
53
 
16
- // Using plain-old promises
17
- const data = await fch(url).then(res => res.json());
18
- console.log(data.name); // "Francisco"
19
- })();
54
+ ```js
55
+ import api from 'fch';
56
+
57
+ const data = await api.get('/');
20
58
  ```
21
59
 
60
+ ## Options
22
61
 
62
+ ```js
63
+ import api, { get, post, put, ... } from 'fch';
23
64
 
24
- ## Better `fetch()`
65
+ // General options with their defaults; most of these are also parameters:
66
+ api.baseUrl = null; // Set an API endpoint
67
+ api.method = 'get'; // Default method to use for api()
68
+ api.headers = {}; // Is merged with the headers on a per-request basis
25
69
 
26
- - Isomorphic fetch() so it works the same in the server as the browser.
27
- - Automatic `JSON.stringify()` and `Content-Type: 'application/json'` for plain objects.
28
- - Await/Async Promise interface works as you know and love.
29
- - Better error handling. `>= 400 and <= 100` will _reject_ the promise with an error instance. Can be caught as normal with `.catch()` or `try {} catch (error) {}`.
30
- - Advanced [promises interface](https://github.com/franciscop/swear) so you can chain operations easily.
31
- - Import with the shorthand for tighter syntax. `import { get, post } from 'fch';`.
70
+ // Control simple variables
71
+ api.dedupe = true; // Avoid parallel GET requests to the same path
72
+ api.output = 'body'; // Return the body; use 'response' for the full response
32
73
 
74
+ // Interceptors
75
+ api.before = req => req;
76
+ api.after = res => res;
77
+ api.error = err => Promise.reject(err);
33
78
 
34
- ## Getting started
79
+ // Similar API to fetch()
80
+ api(url, { method, body, headers, ... });
35
81
 
36
- Install it in your project:
82
+ // Our highly recommended style:
83
+ api.get(url, { headers, ... });
84
+ api.post(url, { body, headers, ... });
85
+ api.put(url, { body, headers, ... });
86
+ // ...
37
87
 
88
+ // Just import/use the method you need
89
+ get(url, { headers, ... });
90
+ post(url, { body, headers, ... });
91
+ put(url, { body, headers, ... });
92
+ // ...
38
93
  ```
39
- npm install fch
94
+
95
+ ### URL
96
+
97
+ This is normally the first argument, though technically you can use both styles:
98
+
99
+ ```js
100
+ // All of these methods are valid
101
+ import api from 'fch';
102
+
103
+ // We strongly recommend using this style for your normal code:
104
+ await api.post('/hello', { body: '...', headers: {} })
105
+
106
+ // Try to avoid these, but they are also valid:
107
+ await api('/hello', { method: 'post', body: '...', headers: {} });
108
+ await api({ url: '/hello', method: 'post', headers: {}, body: '...' });
109
+ await api.post({ url: '/hello', headers: {}, body: '...' });
40
110
  ```
41
111
 
42
- Then import it to be able to use it in your code:
112
+ It can be either absolute or relative, in which case it'll use the local one in the page. It's recommending to set `baseUrl`:
43
113
 
44
114
  ```js
45
- const { get, post, ... } = require('fch'); // Old school
46
- import fch, { get, post, ... } from 'fch'; // New wave
115
+ import api from 'fch';
116
+ api.baseUrl = 'https//api.filemon.io/';
117
+ api.get('/hello');
118
+ // Called https//api.filemon.io/hello
47
119
  ```
48
120
 
49
- Alternatively, include it straight from the CDN for front-end:
121
+ ### Body
122
+
123
+ The `body` can be a string, a FormData instance or a plain object. If it's an object, it'll be stringified and the header `application/json` will be added. Otherwise it'll be sent as plain text:
50
124
 
51
- ```html
52
- <script src="https://cdn.jsdelivr.net/npm/fch"></script>
53
- <script>
54
- const { get, post, ... } = fch;
55
- </script>
125
+ ```js
126
+ import api from 'api';
127
+
128
+ // Sending plain text
129
+ await api.post('/houses', { body: 'plain text' });
130
+
131
+ // Will JSON.stringify it internally, and add the JSON headers
132
+ await api.post('/houses', { body: { id: 1, name: 'Cute Cottage' } });
133
+
134
+ // Send it as FormData
135
+ form.onsubmit = e => {
136
+ await api.post('/houses', { body: new FormData(e.target) });
137
+ };
56
138
  ```
57
139
 
58
140
 
141
+ ### Headers
59
142
 
60
- ## Examples
143
+ You can define headers globally, in which case they'll be added to every request, or locally, so that they are only added to the current request. You can also add them in the `before` callback:
61
144
 
62
- Posting some data as JSON and reading the JSON response:
63
145
 
64
146
  ```js
65
- // With this library fetch
66
- import { post } from 'fch';
67
- const data = await post('/url', { body: { a: 'b' } }).json();
68
- console.log(data);
147
+ import api from 'fch';
148
+ api.headers.abc = 'def';
149
+
150
+ api.get('/helle', { headers: { ghi: 'jkl' } });
151
+ // Total headers on the request:
152
+ // { abc: 'def', ghi: 'jkl' }
69
153
  ```
70
154
 
155
+
156
+ ### Output
157
+
158
+ This controls whether the call returns just the body (default), or the whole response. It can be controlled globally or on a per-request basis:
159
+
71
160
  ```js
72
- // Native example, much longer and cumbersome:
73
- const res = await fetch('/url', {
74
- method: 'POST',
75
- body: JSON.stringify({ a: 'b' }),
76
- headers: { 'content-type': 'application/json; charset=utf-8' }
161
+ import api from 'fch';
162
+
163
+ // "body" (default) or "response"
164
+ api.output = 'body';
165
+
166
+ // Return only the body, this is the default
167
+ const body = await api.get('/data');
168
+
169
+ // Return the whole response (with .body):
170
+ const response = await api.get('/data', { output: 'response' });
171
+
172
+ // Throws error
173
+ const invalid = await api.get('/data', { output: 'invalid' });
174
+ ```
175
+
176
+
177
+ ### Dedupe
178
+
179
+ When you perform a GET request to a given URL, but another GET request *to the same* URL is ongoing, it'll **not** create a new request and instead reuse the response when the first one is finished:
180
+
181
+ ```js
182
+ fetch.mockOnce("a").mockOnce("b");
183
+ const res = await Promise.all([fch("/a"), fch("/a")]);
184
+
185
+ // Reuses the first response if two are launched in parallel
186
+ expect(res).toEqual(["a", "a"]);
187
+ ```
188
+
189
+ You can disable this by setting either the global `fch.dedupe` option to `false` or by passing an option per request:
190
+
191
+ ```js
192
+ // Globally set it for all calls
193
+ fch.dedupe = true; // [DEFAULT] Dedupes GET requests
194
+ fch.dedupe = false; // All fetch() calls trigger a network call
195
+
196
+ // Set it on a per-call basis
197
+ fch('/a', { dedupe: true }); // [DEFAULT] Dedupes GET requests
198
+ fch('/a', { dedupe: false }) // All fetch() calls trigger a network call
199
+ ```
200
+
201
+ > We do not support deduping other methods right now besides `GET` right now
202
+
203
+ Note that opting out of deduping a request will _also_ make that request not be reusable, see this test for details:
204
+
205
+ ```js
206
+ it("can opt out locally", async () => {
207
+ fetch.once("a").once("b").once("c");
208
+ const res = await Promise.all([
209
+ fch("/a"),
210
+ fch("/a", { dedupe: false }),
211
+ fch("/a"), // Reuses the 1st response, not the 2nd one
212
+ ]);
213
+
214
+ expect(res).toEqual(["a", "b", "a"]);
215
+ expect(fetch.mock.calls.length).toEqual(2);
77
216
  });
78
- if (!res.ok) throw new Error(res.statusText);
79
- const data = await res.json();
80
- console.log(data);
217
+ ```
218
+
219
+ ### Interceptors
220
+
221
+ You can also easily add the interceptors `before`, `after` and `error`:
222
+
223
+ ```js
224
+ // Perform an action or request transformation before the request is sent
225
+ fch.before = async req => {
226
+ // Normalized request ready to be sent
227
+ ...
228
+ return req;
229
+ };
230
+
231
+ // Perform an action or data transformation after the request is finished
232
+ fch.after = async res => {
233
+ // Full response as just after the request is made
234
+ ...
235
+ return res;
236
+ };
237
+
238
+ // Perform an action or data transformation when an error is thrown
239
+ fch.error = async err => {
240
+ // Need to re-throw if we want to throw on error
241
+ ...
242
+ throw err;
243
+
244
+ // OR, resolve it as if it didn't fail
245
+ return err.response;
246
+
247
+ // OR, resolve it with a custom value
248
+ return { message: 'Request failed with a code ' + err.response.status };
249
+ };
250
+ ```
251
+
252
+
253
+
254
+ ### Advanced promise interface
255
+
256
+ This library uses [the `swear` promise interface](https://www.npmjs.com/swear), which allows you to query operations seamlessly on top of your promise:
257
+
258
+ ```js
259
+ import api from "fch";
260
+
261
+ // You can keep performing actions like it was sync
262
+ const firstFriendName = await api.get("/friends")[0].name;
263
+ console.log(firstFriendName);
264
+ ```
265
+
266
+ ### Define shared options
267
+
268
+ You can also define values straight away:
269
+
270
+ ```js
271
+ import api from "fch";
272
+
273
+ api.baseUrl = "https://pokeapi.co/";
274
+
275
+ const mew = await api.get("/pokemon/150");
276
+ console.log(mew);
277
+ ```
278
+
279
+ If you prefer Axios' style of outputting the whole response, you can do:
280
+
281
+ ```js
282
+ // Default, already only returns the data on a successful call
283
+ api.output = "data";
284
+ const name = await api.get("/users/1").name;
285
+
286
+ // Axios-like
287
+ api.output = "response";
288
+ const name = await api.get("/users/1").data.name;
289
+ ```
290
+
291
+
292
+ ## How to
293
+
294
+ ### Stop errors from throwing
295
+
296
+ While you can handle this on a per-request basis, if you want to overwrite the global behavior you can write a interceptor:
297
+
298
+ ```js
299
+ import fch from 'fch';
300
+ fch.error = error => error.response;
301
+
302
+ const res = await fch('/notfound');
303
+ expect(res.status).toBe(404);
304
+ ```
305
+
306
+ ### Return the full response
307
+
308
+ By default a successful request will just return the data. However this one is configurable on a global level:
309
+
310
+ ```js
311
+ import fch from 'fch';
312
+ fch.output = 'response'; // Valid values are 'body' (default) or 'response'
313
+
314
+ const res = await fch('/hello');
315
+ ```
316
+
317
+ Or on a per-request level:
318
+
319
+ ```js
320
+ import fch from 'fch';
321
+
322
+ // Valid values are 'body' (default) or 'response'
323
+ const res = await fch('/hello', { output: 'response' });
324
+
325
+ // Does not affect others
326
+ const body = await fch('/hello');
327
+ ```
328
+
329
+
330
+ ### Set a base URL
331
+
332
+ There's a configuration parameter for that:
333
+
334
+ ```js
335
+ import fch from 'fch';
336
+ fch.baseUrl = 'https://api.filemon.io/';
337
+
338
+ // Calls "https://api.filemon.io/blabla"
339
+ const body = await fch.get('/blabla');
340
+ ```
341
+
342
+
343
+ ### Set the authorization headers
344
+
345
+ You can set that globally as a header:
346
+
347
+ ```js
348
+ import fch from 'fch';
349
+ fch.headers.Authorization = 'bearer abc';
350
+
351
+ const me = await fch('/users/me');
352
+ ```
353
+
354
+ Or globally on a per-request basis, for example if you take the value from localStorage:
355
+
356
+ ```js
357
+ import fch from 'fch';
358
+
359
+ // All the requests will add the Authorization header when the token is
360
+ // in localStorage
361
+ fch.before = req => {
362
+ if (localStorage.get('token')) {
363
+ req.headers.Authorization = 'bearer ' + localStorage.get('token');
364
+ }
365
+ return req;
366
+ };
367
+
368
+ const me = await fch('/users/me');
369
+ ```
370
+
371
+ Or on a per-request basis, though we wouldn't recommend this:
372
+
373
+ ```js
374
+ import fch from 'fch';
375
+
376
+ const me = await fch('/users/me', { headers: { Authorization: 'bearer abc' } });
81
377
  ```
package/.babelrc DELETED
@@ -1,3 +0,0 @@
1
- {
2
- "presets": ["env"]
3
- }
package/fetch.min.js DELETED
@@ -1 +0,0 @@
1
- (function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.fch=factory())})(this,function(){"use strict";const resolve=async value=>{value=await value;if(Array.isArray(value)){return await Promise.all(value.map(resolve))}return value};const reject=message=>Promise.reject(new Error(message));const regexpCallback=cb=>cb instanceof RegExp?cb.test.bind(cb):cb;const callback=(cb,self)=>(...args)=>regexpCallback(cb).call(self,...args);const extend=(cb,self)=>async(value,i,all)=>({value:value,extra:await callback(cb,self)(value,i,all)});const extraUp=({extra:extra})=>extra;const valueUp=({value:value})=>value;const extendArray={every:async(obj,cb,self)=>{for(let i=0;i<obj.length;i++){const found=await callback(cb,self)(obj[i],i,obj);if(!found)return false}return true},filter:async(obj,cb,self)=>{const data=await resolve(obj.map(extend(cb,self)));return data.filter(extraUp).map(valueUp)},find:async(obj,cb,self)=>{for(let i=0;i<obj.length;i++){const found=await callback(cb,self)(obj[i],i,obj);if(found)return obj[i]}},findIndex:async(obj,cb,self)=>{for(let i=0;i<obj.length;i++){const found=await callback(cb,self)(obj[i],i,obj);if(found)return i}return-1},forEach:async(obj,cb,self)=>{await resolve(obj.map(extend(cb,self)));return obj},reduce:async(obj,cb,init)=>{const hasInit=typeof init!=="undefined";if(!hasInit)init=obj[0];for(let i=hasInit?0:1;i<obj.length;i++){init=await callback(cb)(init,obj[i],i,obj)}return init},reduceRight:async(obj,cb,init)=>{const hasInit=typeof init!=="undefined";if(!hasInit)init=obj[obj.length-1];for(let i=obj.length-(hasInit?1:2);i>=0;i--){init=await callback(cb)(init,obj[i],i,obj)}return init},some:async(obj,cb,self)=>{for(let i=0;i<obj.length;i++){const found=await callback(cb,self)(obj[i],i,obj);if(found)return true}return false}};const getter=(obj,extend)=>(target,key)=>{if(key==="then")return(...args)=>{return resolve(obj).then(...args)};if(key==="catch")return(...args)=>{return root(resolve(obj).catch(...args))};return func(resolve(obj).then(obj=>{if(typeof key==="symbol")return obj[key];if(key in extend){return func((...args)=>extend[key](obj,...args),extend)}if(typeof obj==="number"&&key in extend.number){return func((...args)=>extend.number[key](obj,...args),extend)}if(typeof obj==="string"&&key in extend.string){return func((...args)=>extend.string[key](obj,...args),extend)}if(Array.isArray(obj)&&key in extend.array){return func((...args)=>extend.array[key](obj,...args),extend)}if(obj[key]&&obj[key].bind){return func(obj[key].bind(obj),extend)}return func(obj[key],extend)}),extend)};const applier=(obj,extend)=>(target,self,args)=>{return func(resolve(obj).then(obj=>{if(typeof obj!=="function"){return reject(`You tried to call "${JSON.stringify(obj)}" (${typeof obj}) as a function, but it is not.`)}return obj(...args)}),extend)};const func=(obj,extend)=>new Proxy(()=>{},{get:getter(obj,extend),apply:applier(obj,extend)});const root=(obj,{number:number,string:string,array:array,...others}={})=>new Proxy({},{get:getter(obj,{number:{...number},string:{...string},array:{...extendArray,...array},...others})});if(typeof require!=="undefined"){require("isomorphic-fetch")}const ongoing=new Map;const hasPlainBody=options=>{if(options.headers["content-type"])return;if(typeof options.body!=="object")return;if(options.body instanceof FormData)return;return true};const fch=(url,options={})=>{options={method:"get",headers:{},...typeof options==="object"?options:{}};if(options.method.toLowerCase()==="get"){if(ongoing.get(url))return ongoing.get(url)}const headers=options.headers;for(let key in headers){const value=headers[key];delete headers[key];headers[key.toLowerCase()]=value}if(hasPlainBody(options)){options.body=JSON.stringify(options.body);headers["content-type"]="application/json; charset=utf-8"}ongoing.set(url,root(fetch(url,{...options,headers:headers}).then(res=>{ongoing.delete(url);if(!res.ok){const error=new Error(res.statusText);error.response=res;return Promise.reject(error)}const mem=new Map;return new Proxy(res,{get:(target,key)=>{if(["then","catch","finally"].includes(key))return res[key];return()=>{if(!mem.get(key)){mem.set(key,target[key]())}return mem.get(key)}}})})));return ongoing.get(url)};fch.head=((url,options={})=>fch(url,{...options,method:"head"}));fch.get=((url,options={})=>fch(url,{...options,method:"get"}));fch.post=((url,options={})=>fch(url,{...options,method:"post"}));fch.patch=((url,options={})=>fch(url,{...options,method:"patch"}));fch.put=((url,options={})=>fch(url,{...options,method:"put"}));fch.del=((url,options={})=>fch(url,{...options,method:"delete"}));return fch});
package/fetch.test.js DELETED
@@ -1,146 +0,0 @@
1
- import fch, { get } from './fetch.min';
2
- global.fetch = require('jest-fetch-mock');
3
-
4
- describe('fetch()', () => {
5
- beforeEach(() => {
6
- fetch.resetMocks();
7
- });
8
-
9
- it('works', async () => {
10
- fetch.once(JSON.stringify({ secret: '12345' }));
11
- const res = await fch('https://google.com/').json();
12
-
13
- expect(res).toEqual({ secret: '12345' });
14
- expect(fetch.mock.calls.length).toEqual(1);
15
- expect(fetch.mock.calls[0][0]).toEqual('https://google.com/');
16
- });
17
-
18
- it('can use the `fetch.get()` shorthand', async () => {
19
- fetch.once('get');
20
- expect(await fch.get('/').text()).toBe('get');
21
- expect(fetch.mock.calls[0][1].method).toEqual('get');
22
- });
23
-
24
- it('can use the `fetch.patch()` shorthand', async () => {
25
- fetch.once('patch');
26
- expect(await fch.patch('/').text()).toBe('patch');
27
- expect(fetch.mock.calls[0][1].method).toEqual('patch');
28
- });
29
-
30
- it('can use the `fetch.put()` shorthand', async () => {
31
- fetch.once('put');
32
- expect(await fch.put('/').text()).toBe('put');
33
- expect(fetch.mock.calls[0][1].method).toEqual('put');
34
- });
35
-
36
- it('can use the `fetch.head()` shorthand', async () => {
37
- fetch.once('head');
38
- expect(await fch.head('/').text()).toBe('head');
39
- expect(fetch.mock.calls[0][1].method).toEqual('head');
40
- });
41
-
42
- it('can use the `fetch.post()` shorthand', async () => {
43
- fetch.once('post');
44
- expect(await fch.post('/').text()).toBe('post');
45
- expect(fetch.mock.calls[0][1].method).toEqual('post');
46
- });
47
-
48
- it('can use the `fetch.del()` shorthand', async () => {
49
- fetch.once('del');
50
- expect(await fch.del('/').text()).toBe('del');
51
- expect(fetch.mock.calls[0][1].method).toEqual('delete');
52
- });
53
-
54
- it('ignores invalid options', async () => {
55
- fetch.once(JSON.stringify({ secret: '12345' }));
56
- const res = await fch('https://google.com/', 10).json();
57
-
58
- expect(res).toEqual({ secret: '12345' });
59
- expect(fetch.mock.calls.length).toEqual(1);
60
- expect(fetch.mock.calls[0][0]).toEqual('https://google.com/');
61
- });
62
-
63
- it('will not overwrite if it is FormData', async () => {
64
- fetch.once(JSON.stringify({ secret: '12345' }));
65
- const res = await fch('/', { method: 'POST', body: new FormData() }).json();
66
-
67
- expect(res).toEqual({ secret: '12345' });
68
- expect(fetch.mock.calls.length).toEqual(1);
69
- const [url, opts] = fetch.mock.calls[0];
70
- expect(opts).toMatchObject({ body: expect.any(FormData) });
71
- });
72
-
73
- it('will not overwrite if content-type is set', async () => {
74
- fetch.once(JSON.stringify({ secret: '12345' }));
75
- const res = await fch('/', {
76
- method: 'POST',
77
- body: { a: 'b'},
78
- headers: { 'Content-Type': 'xxx' }
79
- }).json();
80
-
81
- expect(res).toEqual({ secret: '12345' });
82
- expect(fetch.mock.calls.length).toEqual(1);
83
- const [url, opts] = fetch.mock.calls[0];
84
- expect(url).toEqual('/');
85
- expect(opts).toMatchObject({
86
- method: 'POST',
87
- body: { a: 'b' },
88
- headers: { 'content-type': 'xxx' }
89
- });
90
- });
91
-
92
- it('will send JSON', async () => {
93
- fetch.once(JSON.stringify({ secret: '12345' }));
94
- const res = await fch('/', { method: 'POST', body: { a: 'b'} }).json();
95
-
96
- expect(res).toEqual({ secret: '12345' });
97
- expect(fetch.mock.calls.length).toEqual(1);
98
- const [url, opts] = fetch.mock.calls[0];
99
- expect(url).toEqual('/');
100
- expect(opts).toMatchObject({
101
- method: 'POST',
102
- body: '{"a":"b"}',
103
- headers: { 'content-type': 'application/json; charset=utf-8' }
104
- });
105
- });
106
-
107
- it('can run in parallel', async () => {
108
- fetch.once(JSON.stringify('a')).once(JSON.stringify('b'));
109
- const res = await Promise.all(['/a', '/b'].map(url => fch(url).json()));
110
-
111
- expect(res).toEqual(['a', 'b']);
112
- expect(fetch.mock.calls.length).toEqual(2);
113
- });
114
-
115
- // There is a bug with node-fetch so this is difficult to test right now
116
- // https://github.com/bitinn/node-fetch/issues/386
117
- it.skip('will not trigger race conditions on get for the same url', async () => {
118
- fetch.once(JSON.stringify('a')).once(JSON.stringify('b'));
119
- const res = await Promise.all(['/', '/'].map(url => fch(url).json()));
120
-
121
- expect(res).toEqual(['a', 'a']);
122
- expect(fetch.mock.calls.length).toEqual(1);
123
- });
124
-
125
- it('can set `accepts` insensitively', async () => {
126
- fetch.once(JSON.stringify({ secret: '12345' }));
127
- const res = await fch('/', { headers: { 'Accepts': 'text/xml' } }).json();
128
-
129
- const [url, opts] = fetch.mock.calls[0];
130
- expect(opts.headers).toEqual({ 'accepts': 'text/xml' });
131
- });
132
-
133
- it('can accept network rejections', async () => {
134
- fetch.mockResponseOnce(JSON.stringify("unauthorized"), { status: 401, ok: false });
135
- await expect(fch('/')).rejects.toMatchObject({
136
- message: 'Unauthorized'
137
- });
138
- });
139
-
140
- it('can accept rejections', async () => {
141
- fetch.mockRejectOnce(new Error('fake error message'));
142
- await expect(fch('/')).rejects.toMatchObject({
143
- message: 'fake error message'
144
- });
145
- });
146
- });