-
Notifications
You must be signed in to change notification settings - Fork 4.2k
/
fetch-all-middleware.js
128 lines (114 loc) · 3.5 KB
/
fetch-all-middleware.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/**
* WordPress dependencies
*/
import { addQueryArgs } from '@wordpress/url';
/**
* Internal dependencies
*/
import apiFetch from '..';
/**
* Apply query arguments to both URL and Path, whichever is present.
*
* @param {import('../types').APIFetchOptions} props
* @param {Record<string, string | number>} queryArgs
* @return {import('../types').APIFetchOptions} The request with the modified query args
*/
const modifyQuery = ( { path, url, ...options }, queryArgs ) => ( {
...options,
url: url && addQueryArgs( url, queryArgs ),
path: path && addQueryArgs( path, queryArgs ),
} );
/**
* Duplicates parsing functionality from apiFetch.
*
* @param {Response} response
* @return {Promise<any>} Parsed response json.
*/
const parseResponse = ( response ) =>
response.json ? response.json() : Promise.reject( response );
/**
* @param {string | null} linkHeader
* @return {{ next?: string }} The parsed link header.
*/
const parseLinkHeader = ( linkHeader ) => {
if ( ! linkHeader ) {
return {};
}
const match = linkHeader.match( /<([^>]+)>; rel="next"/ );
return match
? {
next: match[ 1 ],
}
: {};
};
/**
* @param {Response} response
* @return {string | undefined} The next page URL.
*/
const getNextPageUrl = ( response ) => {
const { next } = parseLinkHeader( response.headers.get( 'link' ) );
return next;
};
/**
* @param {import('../types').APIFetchOptions} options
* @return {boolean} True if the request contains an unbounded query.
*/
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.
*
* @type {import('../types').APIFetchMiddleware}
*/
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 apiFetch( {
...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 = /** @type {any[]} */ ( [] ).concat( results );
while ( nextPage ) {
const nextResponse = await apiFetch( {
...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;