-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement fetchAllMiddleware to handle per_page=-1 through pagination (…
- Loading branch information
1 parent
a8fe865
commit dee3dcf
Showing
10 changed files
with
166 additions
and
191 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,7 @@ | ||
## 2.1.0 (Unreleased) | ||
|
||
- Support `per_page=-1` paginated requests. | ||
|
||
## 2.0.0 (2018-09-05) | ||
|
||
### Breaking Change | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
93 changes: 93 additions & 0 deletions
93
packages/api-fetch/src/middlewares/fetch-all-middleware.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { addQueryArgs } from '@wordpress/url'; | ||
|
||
// Apply query arguments to both URL and Path, whichever is present. | ||
const modifyQuery = ( { path, url, ...options }, queryArgs ) => ( { | ||
...options, | ||
url: url && addQueryArgs( url, queryArgs ), | ||
path: path && addQueryArgs( path, queryArgs ), | ||
} ); | ||
|
||
// Duplicates parsing functionality from apiFetch. | ||
const parseResponse = ( response ) => response.json ? | ||
response.json() : | ||
Promise.reject( response ); | ||
|
||
const parseLinkHeader = ( linkHeader ) => { | ||
if ( ! linkHeader ) { | ||
return {}; | ||
} | ||
const match = linkHeader.match( /<([^>]+)>; rel="next"/ ); | ||
return match ? { | ||
next: match[ 1 ], | ||
} : {}; | ||
}; | ||
|
||
const getNextPageUrl = ( response ) => { | ||
const { next } = parseLinkHeader( response.headers.get( 'link' ) ); | ||
return next; | ||
}; | ||
|
||
const requestContainsUnboundedQuery = ( options ) => { | ||
const pathIsUnbounded = options.path && options.path.indexOf( 'per_page=-1' ) !== -1; | ||
const urlIsUnbounded = options.url && options.url.indexOf( 'per_page=-1' ) !== -1; | ||
return pathIsUnbounded || urlIsUnbounded; | ||
}; | ||
|
||
// The REST API enforces an upper limit on the per_page option. To handle large | ||
// collections, apiFetch consumers can pass `per_page=-1`; this middleware will | ||
// then recursively assemble a full response array from all available pages. | ||
const fetchAllMiddleware = async ( options, next ) => { | ||
if ( options.parse === false ) { | ||
// If a consumer has opted out of parsing, do not apply middleware. | ||
return next( options ); | ||
} | ||
if ( ! requestContainsUnboundedQuery( options ) ) { | ||
// If neither url nor path is requesting all items, do not apply middleware. | ||
return next( options ); | ||
} | ||
|
||
// Retrieve requested page of results. | ||
const response = await next( { | ||
...modifyQuery( options, { | ||
per_page: 100, | ||
} ), | ||
// Ensure headers are returned for page 1. | ||
parse: false, | ||
} ); | ||
|
||
const results = await parseResponse( response ); | ||
|
||
if ( ! Array.isArray( results ) ) { | ||
// We have no reliable way of merging non-array results. | ||
return results; | ||
} | ||
|
||
let nextPage = getNextPageUrl( response ); | ||
|
||
if ( ! nextPage ) { | ||
// There are no further pages to request. | ||
return results; | ||
} | ||
|
||
// Iteratively fetch all remaining pages until no "next" header is found. | ||
let mergedResults = [].concat( results ); | ||
while ( nextPage ) { | ||
const nextResponse = await next( { | ||
...options, | ||
// Ensure the URL for the next page is used instead of any provided path. | ||
path: undefined, | ||
url: nextPage, | ||
// Ensure we still get headers so we can identify the next page. | ||
parse: false, | ||
} ); | ||
const nextResults = await parseResponse( nextResponse ); | ||
mergedResults = mergedResults.concat( nextResults ); | ||
nextPage = getNextPageUrl( nextResponse ); | ||
} | ||
return mergedResults; | ||
}; | ||
|
||
export default fetchAllMiddleware; |
51 changes: 51 additions & 0 deletions
51
packages/api-fetch/src/middlewares/test/fetch-all-middleware.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
/** | ||
* Internal dependencies | ||
*/ | ||
import fetchAllMiddleware from '../fetch-all-middleware'; | ||
|
||
describe( 'Fetch All Middleware', async () => { | ||
it( 'should defer with the same options to the next middleware', async () => { | ||
expect.hasAssertions(); | ||
const originalOptions = { path: '/posts' }; | ||
const next = ( options ) => { | ||
expect( options ).toBe( originalOptions ); | ||
return Promise.resolve( 'ok' ); | ||
}; | ||
|
||
await fetchAllMiddleware( originalOptions, next ); | ||
} ); | ||
|
||
it( 'should paginate the request', async () => { | ||
expect.hasAssertions(); | ||
const originalOptions = { url: '/posts?per_page=-1' }; | ||
let counter = 1; | ||
const next = ( options ) => { | ||
if ( counter === 1 ) { | ||
expect( options.url ).toBe( '/posts?per_page=100' ); | ||
} else { | ||
expect( options.url ).toBe( '/posts?per_page=100&page=2' ); | ||
} | ||
const response = Promise.resolve( { | ||
status: 200, | ||
headers: { | ||
get() { | ||
return options.url === '/posts?per_page=100' ? | ||
'</posts?per_page=100&page=2>; rel="next"' : | ||
''; | ||
}, | ||
}, | ||
json() { | ||
return Promise.resolve( [ 'item' ] ); | ||
}, | ||
} ); | ||
|
||
counter++; | ||
|
||
return response; | ||
}; | ||
|
||
const result = await fetchAllMiddleware( originalOptions, next ); | ||
|
||
expect( result ).toEqual( [ 'item', 'item' ] ); | ||
} ); | ||
} ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.