From a2ce3cacc8751a2fc11fedffd0bda2c4b34807a1 Mon Sep 17 00:00:00 2001 From: Alan Friedman Date: Fri, 2 Mar 2018 07:56:29 -0500 Subject: [PATCH 01/11] Use custom fetch handler if provided --- src/middleware.js | 5 ++-- src/validation.js | 12 ++++++++-- test/index.js | 59 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/src/middleware.js b/src/middleware.js index 0077129..7dce3fb 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -37,11 +37,12 @@ function apiMiddleware({ getState }) { // Parse the validated RSAA action const callAPI = action[RSAA]; - var { endpoint, body, headers, options = {} } = callAPI; + var { endpoint, body, headers, options = {}, fetch: customFetch } = callAPI; const { method, credentials, bailout, types } = callAPI; const [requestType, successType, failureType] = normalizeTypeDescriptors( types ); + let doFetch = customFetch || fetch; // Should we bail out? try { @@ -148,7 +149,7 @@ function apiMiddleware({ getState }) { try { // Make the API call - var res = await fetch(endpoint, { + var res = await doFetch(endpoint, { ...options, method, body: body || undefined, diff --git a/src/validation.js b/src/validation.js index cc63dae..95feb1b 100644 --- a/src/validation.js +++ b/src/validation.js @@ -60,7 +60,8 @@ function validateRSAA(action) { 'headers', 'credentials', 'bailout', - 'types' + 'types', + 'fetch', ]; const validMethods = [ 'GET', @@ -103,7 +104,8 @@ function validateRSAA(action) { options, credentials, types, - bailout + bailout, + fetch } = callAPI; if (typeof endpoint === 'undefined') { validationErrors.push('[RSAA] must have an endpoint property'); @@ -186,6 +188,12 @@ function validateRSAA(action) { } } + if (typeof fetch !== 'undefined') { + if (typeof fetch !== 'function') { + validationErrors.push('[RSAA].fetch property must be a function'); + } + } + return validationErrors; } diff --git a/test/index.js b/test/index.js index d9d26a9..8e7ffec 100644 --- a/test/index.js +++ b/test/index.js @@ -1921,3 +1921,62 @@ test('apiMiddleware must dispatch a failure FSA on an unsuccessful API call with t.plan(8); actionHandler(anAction); }); + +test('apiMiddleware must use a [RSAA].fetch custom fetch wrapper when present', t => { + const asyncWorker = async () => 'Done!'; + const responseBody = { + id: 1, + name: 'Alan', + error: false, + } + const api = nock('http://127.0.0.1') + .get('/api/users/1') + .reply(200, responseBody); + const anAction = { + [RSAA]: { + endpoint: 'http://127.0.0.1/api/users/1', + method: 'GET', + fetch: async (endpoint, opts) => { + t.pass('custom fetch handler called'); + + // Simulate some async process like retrieving cache + await asyncWorker(); + + const res = await fetch(endpoint, opts); + + return { + ...res, + // Custom `ok` check + ok: !responseBody.error, + json: async () => ({ + ...responseBody, + // Custom `json` response + foo: 'bar', + }), + } + }, + types: ['REQUEST', 'SUCCESS', 'FAILURE'] + } + }; + const doGetState = () => {}; + const nextHandler = apiMiddleware({ getState: doGetState }); + const doNext = action => { + switch (action.type) { + case 'SUCCESS': + t.deepEqual( + action.payload, + { + ...responseBody, + foo: 'bar', + }, + 'custom response passed to the next handler' + ); + break; + } + } + + const actionHandler = nextHandler(doNext); + + t.plan(2); + actionHandler(anAction); +}); From a60ece4eb5261f6c903d090aacbf955b624398ee Mon Sep 17 00:00:00 2001 From: Alan Friedman Date: Fri, 2 Mar 2018 07:59:12 -0500 Subject: [PATCH 02/11] Use const --- src/middleware.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware.js b/src/middleware.js index 7dce3fb..e93cb61 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -42,7 +42,7 @@ function apiMiddleware({ getState }) { const [requestType, successType, failureType] = normalizeTypeDescriptors( types ); - let doFetch = customFetch || fetch; + const doFetch = customFetch || fetch; // Should we bail out? try { From 6e756cdc1a8b0a08cae2e6dd4cee1f26c773e892 Mon Sep 17 00:00:00 2001 From: Alan Friedman Date: Fri, 2 Mar 2018 08:05:43 -0500 Subject: [PATCH 03/11] Fix lint errors --- src/middleware.js | 8 +++++++- src/validation.js | 2 +- test/index.js | 16 ++++++++-------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/middleware.js b/src/middleware.js index e93cb61..584d163 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -37,7 +37,13 @@ function apiMiddleware({ getState }) { // Parse the validated RSAA action const callAPI = action[RSAA]; - var { endpoint, body, headers, options = {}, fetch: customFetch } = callAPI; + var { + endpoint, + body, + headers, + options = {}, + fetch: customFetch + } = callAPI; const { method, credentials, bailout, types } = callAPI; const [requestType, successType, failureType] = normalizeTypeDescriptors( types diff --git a/src/validation.js b/src/validation.js index 95feb1b..aec300d 100644 --- a/src/validation.js +++ b/src/validation.js @@ -61,7 +61,7 @@ function validateRSAA(action) { 'credentials', 'bailout', 'types', - 'fetch', + 'fetch' ]; const validMethods = [ 'GET', diff --git a/test/index.js b/test/index.js index 8e7ffec..ec7ff6b 100644 --- a/test/index.js +++ b/test/index.js @@ -1927,8 +1927,8 @@ test('apiMiddleware must use a [RSAA].fetch custom fetch wrapper when present', const responseBody = { id: 1, name: 'Alan', - error: false, - } + error: false + }; const api = nock('http://127.0.0.1') .get('/api/users/1') .reply(200, responseBody); @@ -1938,7 +1938,7 @@ test('apiMiddleware must use a [RSAA].fetch custom fetch wrapper when present', method: 'GET', fetch: async (endpoint, opts) => { t.pass('custom fetch handler called'); - + // Simulate some async process like retrieving cache await asyncWorker(); @@ -1951,9 +1951,9 @@ test('apiMiddleware must use a [RSAA].fetch custom fetch wrapper when present', json: async () => ({ ...responseBody, // Custom `json` response - foo: 'bar', - }), - } + foo: 'bar' + }) + }; }, types: ['REQUEST', 'SUCCESS', 'FAILURE'] } @@ -1967,13 +1967,13 @@ test('apiMiddleware must use a [RSAA].fetch custom fetch wrapper when present', action.payload, { ...responseBody, - foo: 'bar', + foo: 'bar' }, 'custom response passed to the next handler' ); break; } - } + }; const actionHandler = nextHandler(doNext); From b09bb725c0cc02b2784737506f481b0bc2bb6839 Mon Sep 17 00:00:00 2001 From: Alan Friedman Date: Fri, 2 Mar 2018 08:38:23 -0500 Subject: [PATCH 04/11] Update test --- test/index.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/test/index.js b/test/index.js index ec7ff6b..5992f43 100644 --- a/test/index.js +++ b/test/index.js @@ -1944,16 +1944,17 @@ test('apiMiddleware must use a [RSAA].fetch custom fetch wrapper when present', const res = await fetch(endpoint, opts); - return { - ...res, - // Custom `ok` check - ok: !responseBody.error, - json: async () => ({ - ...responseBody, - // Custom `json` response - foo: 'bar' - }) + const customJson = { + ...(await res.json()), + foo: 'bar' }; + + return new Response(JSON.stringify(customJson), { + status: 200, + headers: { + 'Content-Type': 'application/json' + } + }); }, types: ['REQUEST', 'SUCCESS', 'FAILURE'] } From c31e022dbd5a8b3ce4335d18a794994acb71f3f9 Mon Sep 17 00:00:00 2001 From: Alan Friedman Date: Fri, 2 Mar 2018 09:29:34 -0500 Subject: [PATCH 05/11] Add example of custom `ok` --- test/index.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/test/index.js b/test/index.js index 5992f43..48c742d 100644 --- a/test/index.js +++ b/test/index.js @@ -1943,18 +1943,21 @@ test('apiMiddleware must use a [RSAA].fetch custom fetch wrapper when present', await asyncWorker(); const res = await fetch(endpoint, opts); + const json = await res.json(); - const customJson = { - ...(await res.json()), - foo: 'bar' - }; - - return new Response(JSON.stringify(customJson), { - status: 200, - headers: { - 'Content-Type': 'application/json' + return new Response( + JSON.stringify({ + ...json, + foo: 'bar' + }), + { + // Example of custom `res.ok` + status: json.error ? 500 : 200, + headers: { + 'Content-Type': 'application/json' + } } - }); + ); }, types: ['REQUEST', 'SUCCESS', 'FAILURE'] } From e819b7bb0cbf81538424a48700537696f08c3709 Mon Sep 17 00:00:00 2001 From: Alan Friedman Date: Fri, 2 Mar 2018 11:33:44 -0500 Subject: [PATCH 06/11] Add tests for fetch validation --- test/index.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/index.js b/test/index.js index 48c742d..2b9376a 100644 --- a/test/index.js +++ b/test/index.js @@ -531,6 +531,32 @@ test('validateRSAA/isValidRSAA must identify conformant RSAAs', t => { ); t.ok(isValidRSAA(action26), '[RSAA].options may be a function (isRSAA)'); + const action27 = { + [RSAA]: { + endpoint: '', + method: 'GET', + types: ['REQUEST', 'SUCCESS', 'FAILURE'], + fetch: () => {} + } + }; + t.ok( + isValidRSAA(action27), + '[RSAA].fetch property must be a function (isRSAA)' + ); + + const action28 = { + [RSAA]: { + endpoint: '', + method: 'GET', + types: ['REQUEST', 'SUCCESS', 'FAILURE'], + fetch: {} + } + }; + t.notOk( + isValidRSAA(action28), + '[RSAA].fetch property must be a function (isRSAA)' + ); + t.end(); }); From f125a43e3f85fb9d111fc59106e1c17d51d632d2 Mon Sep 17 00:00:00 2001 From: Alan Friedman Date: Sat, 3 Mar 2018 12:23:51 -0500 Subject: [PATCH 07/11] Simplify custom fetch assignment --- src/middleware.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/middleware.js b/src/middleware.js index 584d163..d9b1a89 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -42,13 +42,12 @@ function apiMiddleware({ getState }) { body, headers, options = {}, - fetch: customFetch + fetch: doFetch = fetch } = callAPI; const { method, credentials, bailout, types } = callAPI; const [requestType, successType, failureType] = normalizeTypeDescriptors( types ); - const doFetch = customFetch || fetch; // Should we bail out? try { From 148f38c36a36b4f71cbfe21606ae073bcba4e11c Mon Sep 17 00:00:00 2001 From: Alan Friedman Date: Sat, 3 Mar 2018 12:24:16 -0500 Subject: [PATCH 08/11] Add test for error response --- test/index.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/test/index.js b/test/index.js index 2b9376a..8ad1d9d 100644 --- a/test/index.js +++ b/test/index.js @@ -2010,3 +2010,48 @@ test('apiMiddleware must use a [RSAA].fetch custom fetch wrapper when present', t.plan(2); actionHandler(anAction); }); + +test.only('apiMiddleware must dispatch correct error payload when custom fetch wrapper returns an error response', t => { + const anAction = { + [RSAA]: { + endpoint: 'http://127.0.0.1/api/users/1', + method: 'GET', + fetch: async (endpoint, opts) => { + return new Response( + JSON.stringify({ + foo: 'bar' + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + } + ); + }, + types: ['REQUEST', 'SUCCESS', 'FAILURE'] + } + }; + const doGetState = () => {}; + const nextHandler = apiMiddleware({ getState: doGetState }); + const doNext = action => { + switch (action.type) { + case 'FAILURE': + t.ok(action.payload instanceof Error); + t.pass('error action dispatched'); + t.deepEqual( + action.payload.response, + { + foo: 'bar' + }, + 'custom response passed to the next handler' + ); + break; + } + }; + + const actionHandler = nextHandler(doNext); + + t.plan(3); + actionHandler(anAction); +}); From 1a41236a29a2b3b689d36913911d1f24e570c8ea Mon Sep 17 00:00:00 2001 From: Alan Friedman Date: Sat, 3 Mar 2018 12:34:31 -0500 Subject: [PATCH 09/11] Remove `test.only` --- test/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/index.js b/test/index.js index 8ad1d9d..400ff78 100644 --- a/test/index.js +++ b/test/index.js @@ -2011,7 +2011,7 @@ test('apiMiddleware must use a [RSAA].fetch custom fetch wrapper when present', actionHandler(anAction); }); -test.only('apiMiddleware must dispatch correct error payload when custom fetch wrapper returns an error response', t => { +test('apiMiddleware must dispatch correct error payload when custom fetch wrapper returns an error response', t => { const anAction = { [RSAA]: { endpoint: 'http://127.0.0.1/api/users/1', From e4d33124431da8a52230cb1e839eacfb487134ce Mon Sep 17 00:00:00 2001 From: Alan Friedman Date: Sat, 3 Mar 2018 12:53:17 -0500 Subject: [PATCH 10/11] Add custom fetch docs --- README.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 31f264c..68facde 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,72 @@ It must be one of the following strings: - `same-origin` only sends cookies for the current domain; - `include` always send cookies, even for cross-origin calls. +#### `[RSAA].fetch` + +A custom [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) implementation, useful for intercepting the fetch request to customize the response status, modify the response payload or skip the request altogether and provide a cached response instead. + +`fetch` must be a function that returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) which resolves with an instance of [Request](https://developer.mozilla.org/en-US/docs/Web/API/Response). + +Example of modifying a request payload and status: + +```js +{ + [RSAA]: { + ... + fetch: async (...args) => { + // `fetch` args may be just a Request instance or [URI, options] (see Fetch API docs above) + const res = await fetch(...args); + const json = await res.json(); + + return new Response( + JSON.stringify({ + ...json, + // Adding to the JSON response + foo: 'bar' + }), + { + // Custom success/error status based on an `error` key in the API response + status: json.error ? 500 : 200, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } + ... + } +} +``` + +Example of skipping the request in favor of a cached response: + +```js +{ + [RSAA]: { + ... + fetch: async (...args) => { + const cached = await getCache('someKey'); + + if (cached) { + // where `cached` is a JSON string: '{"foo": "bar"}' + return new Response(cached, + { + status: 200, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } + + // Fetch as usual if not cached + return fetch(...args); + } + ... + } +} +``` + ### Bailing out In some cases, the data you would like to fetch from the server may already be cached in your Redux store. Or you may decide that the current user does not have the necessary permissions to make some request. @@ -563,11 +629,12 @@ The `[RSAA]` property MAY - have a `headers` property, - have an `options` property, - have a `credentials` property, -- have a `bailout` property. +- have a `bailout` property, +- have a `fetch` property. The `[RSAA]` property MUST NOT -- include properties other than `endpoint`, `method`, `types`, `body`, `headers`, `options`, `credentials`, and `bailout`. +- include properties other than `endpoint`, `method`, `types`, `body`, `headers`, `options`, `credentials`, `bailout` and `fetch`. #### `[RSAA].endpoint` @@ -599,6 +666,10 @@ The optional `[RSAA].credentials` property MUST be one of the strings `omit`, `s The optional `[RSAA].bailout` property MUST be a boolean or a function. +#### `[RSAA].fetch` + +The optional `[RSAA].fetch` property MUST be a function. + #### `[RSAA].types` The `[RSAA].types` property MUST be an array of length 3. Each element of the array MUST be a string, a `Symbol`, or a type descriptor. From b830a6f97fd113ce9b74afde9e2d8bdbe0923952 Mon Sep 17 00:00:00 2001 From: Alan Friedman Date: Sat, 3 Mar 2018 13:18:15 -0500 Subject: [PATCH 11/11] Update fetch docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 68facde..067c685 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,7 @@ It must be one of the following strings: A custom [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) implementation, useful for intercepting the fetch request to customize the response status, modify the response payload or skip the request altogether and provide a cached response instead. -`fetch` must be a function that returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) which resolves with an instance of [Request](https://developer.mozilla.org/en-US/docs/Web/API/Response). +If provided, the fetch option must be a function that conforms to the Fetch API. Otherwise, the global fetch will be used. Example of modifying a request payload and status: