The Sonatype REST API paginates data in its response with a continuation token
placed inside the response body; if the continuation token is null
, you know you're on the last page of results.
I would like to recursively fetch all records from the endpoint with fetch
(unless there is a better strategy?), updating the url using a query parameter on each recursive iteration and calling the recursive fetch
on it, and building a storage structure containing the complete list of records in the function. I have been unsuccessful.
To get the first page of records, this works:
const requestUrl = `https://{baseURL}/service/rest/v1/components?repository=myRepo&group=myGroup`;
// returns first 10 results
async function fetchData(url) {
let headers = new Headers();
headers.append("Accept", "application/json");
headers.append(
"Authorization",
"Basic <BASE64-STRING-HERE>"
);
const requestOptions = {
method: "GET",
headers: headers,
redirect: "follow",
};
// Fetch request and parse as JSON
const response = await fetch(url, requestOptions);
let data = await response.json();
return data;
}
// called at `/api/get-data`
export default async function getData(req, res) {
try {
let result = await fetchData(requestUrl);
res.status(200).json({ result });
} catch (err) {
res.status(500).json({ error: "failed to load data" });
}
}
This returns an object containing an array of 10 records and the continuation token
, a string, e.g.
{"items":[{"id": 1, "name": "itemOne"}, {"id": 2, "name": "itemTwo"}, etc.],"continuationToken":"07929bf0c78cf6c92d4acd0bf5823d80"}}
On the last page of records, the continuationToken
is null
:
{"items":[{"id": 99, "name": "itemNinetyNine"}, {"id": 100, "name": "itemOneHundred"}, etc.],"continuationToken": null}}
To get the complete set of records using recursion, this is the strategy I have tried, but haven't been successful:
async function recursivelyFetchData(url) {
let headers = new Headers();
headers.append("Accept", "application/json");
headers.append(
"Authorization",
"Basic <BASE-64-STRING>"
);
const requestOptions = {
method: "GET",
headers: headers,
redirect: "follow",
};
try {
// Fetch request and parse as JSON
const response = await fetch(url, requestOptions);
let data = await response.json();
// create a storage array
let storage = [];
// Set a variable for the next page url
let nextPageUrl;
// if the initial response contains a continuation token, start to recurse
if (data.continuationToken) {
// remove the query param so we don't keep adding it to the url string on each iteration
let strippedUrl = url.replace(/&continuationToken=.*/g, "");
// create the url with the query param
nextPageUrl =
strippedUrl `&continuationToken=` data.continuationToken;
// store the next response
let nextPageResponse = await recursivelyFetchData(
nextPageUrl,
requestOptions
);
// parse
let nextPageData = await nextPageResponse.json();
// add to storage
storage = [...data, ...nextPageData.items];
}
// break recursion
return storage;
} catch (err) {
res.status(500).json({ error: "failed to load data" });
}
}
export default async function getData(req, res) {
try {
let result = await recursivelyFetchData(requestUrl);
res.status(200).json({ result });
} catch (err) {
res.status(500).json({ error: "failed to load data in getPackages" });
}
}
I've tried the approaches found here, here, here, here, and here, but this API is different from other paginated responses in that it relies on a continuation token in the response body, and I can't for the life of me figure out how to recursively add the query parameter to the url, continue iterating until the continuation token is null
, then break recursion.
Any help is appreciated.
CodePudding user response:
Here is what I think is a simple approach. fetchAllPages
fetches the first page, and then checks to see if the result has a non-nil continuation
token. If it doesn't we just returns the items from the result. But if it does, we merge that token into a new url and recur, combining the current items with the newly-fetched ones into a single array.
Relying on a helper addToken
function that builds our URL, it looks like this:
const fetchAllPages = (url) => fetch (url)
.then (res => res .json ())
.then (
({items, continuationToken: token}) => token
? fetchAllPages (addToken (url, token)) .then (newItems => [...items, ...newItems])
: items
)
We alter it slightly to show where your header manipulation and such would fit, in a snippet that uses a dummy fetch
function that returns item0
- item33
paginated in groups of ten and an implementation of addToken
that does URL manipulation using DOM methods (rather than string parsing/hacking):
const addToken = (base, token) => {
const url = new URL (base)
const searchParams = new URLSearchParams (url .search)
searchParams .set ('continuationToken', token)
url .search = searchParams
return url .toString ()
}
const fetchAllPages = (url) => {
// configure fetch parameters (headers, etc.)
return fetch (url, /*headers, whatever*/)
.then (res => res .json ())
.then (
({items, continuationToken: token}) => token
? fetchAllPages (addToken (url, token)) .then (newItems => [...items, ...newItems])
: items
)
}
fetchAllPages ('https://baseURL.com/service/rest/v1/components?repository=myRepo&group=myGroup')
.then (console .log)
.catch (console .warn)
.as-console-wrapper {max-height: 100% !important; top: 0}
<script>
// dummy version of `fetch` to demo without a real server
const fetch = (() => {
const data = Array .from ({length: 34}, (_, id) => ({id, name: `item ${id}`}))
const pageSize = 10
return async (s) => {
console .log (`fetching '${s}'`)
const url = new URL (s)
const searchParams = new URLSearchParams (url .search)
const start = searchParams .get ('continuationToken') || '0'
const startIndex = data .findIndex (({id}) => id == start)
const token = startIndex < 0 || (startIndex pageSize >= data.length) ? null : data [startIndex pageSize] .id
return {json: () => ({
items: data .slice (startIndex, startIndex pageSize),
continuationToken: token ? String (token) : null
})}
}
})()
</script>