Nodejs Pagination of API Call Using Offset - node.js

I apologize if this question is documented already, please point me to those resources.
I have a nodejs app making an api call to Untappd and per most public APIs, I'm restricted to a max number of items returned in the call, in this case 50 is the max. I'd like to set up the pagination using Offset (Skip) so that I could move through the 600+ items instead of just the 50.
What Works
I currently have the pagination working to view the first 50 items through these different pieces...
API call on server.js
const untappdAPI = { method: 'GET',
url: 'https://api.untappd.com/v4/user/beers/username',
qs:
{ access_token: 'abc123'
,limit:'50'
}
};
app.get with pagination on server.js
app.get('/untappd', function (req, res) {
try {
request(untappdAPI, function (error, response, body) {
if (error) throw new Error(error);
const untappdBeers = JSON.parse(body);
const utBeerList = untappdBeers.response.beers.items.map(item => item );
//pagination
const perPage = 5;
let currentPage = 1;
const totalBeerList = utBeerList.length;
const pageCount = Math.ceil(totalBeerList / perPage);
if(req.query.page) {
currentPage = parseInt(req.query.page, 10);
}
const start = (currentPage - 1) * perPage;
const end = currentPage * perPage;
res.render('untappd.ejs', {
utBeerList:utBeerList.slice(start, end),
perPage: perPage,
pageCount: pageCount,
currentPage: currentPage,
});
});
} catch(e) {
console.log("Something went wrong", e)
}
});
And then the items render on a page called untappd.ejs and the working pagination is provided with this code
EJS Client Pagination on untappd.ejs
<div id="pagination">
<% if (pageCount > 1) { %>
<ul class="pagination">
<% if (currentPage > 1) { %>
<li>«</li>
<% } %>
<% var i = 1;
if (currentPage > 5) {
i = +currentPage - 4;
} %>
<% if (i !== 1) { %>
<li>...</li>
<% } %>
<% for (i; i<=pageCount; i++) { %>
<% if (currentPage == i) { %>
<li class="active"><span><%= i %> <span class="sr-only">(current)</span></span></li>
<% } else { %>
<li><%= i %></li>
<% } %>
<% if (i == (+currentPage + 4)) { %>
<li>...</li>
<% break; } %>
<% } %>
<% if (currentPage != pageCount) { %>
<li>»</li>
<% } %>
</ul>
<% } %>
</div>
Again, the above code is all well and good, I get functioning pagination with 5 items per page and 10 pages to paginate through, but I am limited to these 50 items. From what I've read about offset, it seems offset would allow me to cycle through the entire set of 600+ items, but I can't find documentation/help that fits this particular scenario.
How do I incorporate 'offset' into what I'm working with in order to paginate through the full list of 600+ items?
Many thanks for your help!
Red

The documentation is quite straight forward (UNLESS you are trying something else... but I'm assuming /v4/search/beer), all you need to do is use the offset and limit that the API provides, that would work like:
offset (int, optional) - The numeric offset that you what results to start
limit (int, optional) - The number of results to return, max of 50, default is 25
this means that
if you want to search beers from 0 to 50, set: offset: 0, limit: 50
if you want to search beers from 51 to 100, set offset: 50, limit: 50
if you want to search beers from 101 to 150, set offset: 100, limit: 50
I would change your code as:
const untappdAPI = (name, currentPage, perPage) => ({ method: 'GET',
url: 'https://api.untappd.com/v4/search/beer',
qs: {
q: name,
access_token: 'abc123',
limit: perPage, // 50
offset: currentPage * perPage // 0*50 = 0 | 1*50 = 50 | 2*50 = 100
}
})
having that as a function, will allow you to simply pass parameters such as
app.get('/untappd', function(req, res) {
const { search, currentPage, perPage } = req.query
const url = untappdAPI(search, currentPage, perPage) // (currentPage - 1) if you start with 1
try {
request(url, (error, response, body) => {
if (error) throw new Error(error)
const apiRes = JSON.parse(body).response
const beers = apiRes.beers.items
const total = apiRes.found // outputs in the API, first item in the response
res.render('untappd.ejs', {
utBeerList: beers,
perPage: perPage,
pageCount: Math.ceil(total / perPage),
currentPage: currentPage,
})
})
} catch (err) {
console.log("Something went wrong", err.message)
}
});
The only thing left to do, is update the HTML, as you can see from the call, we need 3 parameters (you can make some static and do not give the user the option to change, for example, items per page, and always display 50 at a time...
»
Note
in your code, you are outputting const pageCount = Math.ceil(totalBeerList / perPage); but the API response gives you the total items that can be retrieved in the first item in the documentation response as the found variable
P.S. I've request access to the API to verify if all this works, I will update the answer soon I have a client id and a secret

Related

Issues displaying data in node js , ejs

I am a newbie
i have been trying to fetch data from an API using node js express and ejs.
My problem is that i cannot display the data in my page.
looks like something is wrong with the way that i render the data . when i console.log my response, i have access to it but it does not seem to want to display
can you help me plz.
I tried to fetch the data like this :
const fetch = require('node-fetch');
exports.allCarte = async (req, res) => {
fetch('https://pokeapi-enoki.netlify.app/')
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then((response) => {
console.log(response)
res.render("../views/home",{
response
})
});
}
and here is what I did in the home.ejs :
<body>
<main>
<ul>
<% for(var i = 0; i < response.length; i++) {%>
<li><%=response[i].name %></li>
<% } %>
<h1>Hi, there!</h1>
</ul>
</main>
<
If you have a look at results from the https://pokeapi-enoki.netlify.app/ API, it is structured something like this:
{
"pokemons": [
{
"id": 1,
"name": "germignon",
...
},
{
"id": 2,
"name": "kaiminus",
...
...which means the array of interest is response.pokemons not just response.
Your home.ejs should be changed like this:
<% for(var i = 0; i < response.pokemons.length; i++) {%>
<li><%=response.pokemons[i].name %></li>
<% } %>

How do I have a variable available to display on my success page, after adding items to a database via a /POST route?

I would like to display the doc.id variable of a successful /POST of data to a route, on the success page that the user will be redirected to afterward. I'm trying to work out how to carry the variable teamId through to the Handlebar template page success.hbs
I've tried making it a variable, and setting up a Handlebar helper to display it, but nothing is working.
/POST route redirecting to success.hbs:
app.post('/create', (req, res) => {
var players = [];
var playerObj = {};
for (let i = 1; i < 21; i++) {
var playerObj = { playerName: req.body[`player${i}Name`], playerNumber: req.body[`player${i}Number`], playerPosition: req.body[`player${i}Position`] };
if (req.body["player" + i + "Name"] === '') {
console.log("Empty player name detected, disregarding");
} else {
players.push(playerObj);
}
}
var newTeam = new Team({
// WEB SETUP BELOW
"team.teamRoster.teamCoach": req.body.coachName,
"team.shortTeamName": req.body.teamShortName,
"team.teamName": req.body.teamName,
"team.teamRoster.players": players
});
newTeam.save().then((doc) => {
var teamId = doc.id;
console.log(teamId);
res.render('success.hbs');
console.log("Team Added");
}, (e) => {
res.status(400).send(e);
});
});
/views/success.hbs
<div class="container-fluid" id="body">
<div class="container" id="page-header">
<h1><span id="headline">Team Added Succesfully</span></h1>
<hr>
<h3><span id="subheadline">Input the following address as a JSON Data Source within vMix.</span></h3>
<span id="content">
<div class="row">
<div class="container col-md-12">
{{{teamId}}}
</div>
</div>
</span>
</div>
<hr>
</div>
I'd like a Handlebar helper to get the doc.id value of the /POST request, and store it as teamId to display on the success page. It's finding nothing at the moment.
Any help is appreciated.
Node.js can pass variables to the handlebars-view like this:
newTeam.save().then((doc) => {
var teamId = doc.id;
console.log(teamId);
res.render('success.hbs', {
teamId
});
console.log("Team Added");
}, (e) => {
res.status(400).send(e);
});

Grouping my data in the view - nodejs

I grab some data from postgresql DB and want to display it in a view, simple enough as below
routes.js
app.get('/fixtures', async (req, res) => {
const fixtures = await queries.getFixtures();
res.render('fixtures', { fixtures });
});
fixtures returns
[ { id: 27,
home_team: 'Chelsea',
away_team: 'Liverpool',
league_name: 'English Premiership',
},
{ id: 25,
home_team: 'Man Utd',
away_team: 'Everton',
league_name: 'English Premiership',
},
{ id: 30,
home_team: 'Istanbul Basaksehir',
away_team: 'Akhisar Belediye',
league_name: 'Turkish Super Lig',
}
]
getFixtures();
async function getFixtures() {
let response;
try {
response = await pool.query('select * from fixtures ORDER BY league_name ASC');
} catch (e) {
console.error('Error Occurred', e);
}
return response.rows;
}
fixtures.ejs
<% fixtures.forEach((fixture) => { %>
<p><%=fixture.league_name %></p>
<p><%= fixture.home_team %> vs <%= fixture.away_team %> </p>
<% }) %>
So the above will output
English Premiership
Chelsea vs Liverpool
English Premiership
Man Utd v Everton
Turkish Super Lig
Istanbul Basaksehir vs Akhisar Belediye
However I would like to group my fixtures by league and would prefer to have the view output as
English Premiership
Chelsea vs Liverpool
Man Utd v Everton
Turkish Super Lig
Istanbul Basaksehir vs Akhisar Belediye
How do I go about achieving this? Is this something at DB query level or something I do in the view? (though probably not the best place to keep logic I guess)
Thanks
I'm not sure what tools/libs you are using but without depending on any libs/tools you can do it using vanilla JavaScript. For example, check the following code:
app.get('/fixtures', async (req, res) => {
const fixtures = await queries.getFixtures();
const grouped = groupByLeagueName(fixtures);
res.render('fixtures', { fixtures: grouped });
});
Then, implement the groupByLeagueName function like this:
function groupByLeagueName(fixtures) {
return fixtures.reduce((result, item) => {
result[item.league_name] = result[item.league_name] || [];
result[item.league_name].push(item);
return result;
}, {});
}
Then in your view you may loop it something like this:
<% for (let leagueName in fixtures) { %>
<p><%= leagueName %></p>
<% fixtures[leagueName].forEach((match, key) => { %>
<p><%= match.home_team %> vs <%= match.away_team %> </p>
<% }) %>
<% } %>

Generating an array in the view and passing it into the controller

It's a little strange but I cannot think of a better way to get it done.
First of all this is my code:
The router to get the view in the first place
router.get('/', function(req, res, next) {
Account.findOne(
{
_id: req.user._id,
},
function(err, acc) {
if (err) {
console.log(err);
}
// console.log(acc.websites);
res.render('reports/index', {
title: 'Reports!',
websites: acc.websites,
user: req.user,
});
}
);
});
The view:
<% include ./../partials/header.ejs %>
<h1 class="text-center">This is your report page</h1>
<form method="post">
<% for(let i=0; i<websites.length; i++){ let website = websites[i]; %>
<fieldset>
<label for="website<%=i%>" class="col-sm-2">Website <%=i+1%></label>
<input name="website<%=i%>" id="website<%=i%>" value="<%=website%>" type="text" />
</fieldset>
<% } %>
Generate report
</form>
<% include ./../partials/footer.ejs %>
The router, that's supposed to fire up after the on click.
router.get('/reports', function(req, res, next) {
if (req.user.isPremium == false) {
// Free user - Single report
var builtWithCall = `https://api.builtwith.com/free1/api.json?KEY=00000000-0000-0000-0000-000000000000&LOOKUP=${website}`;
let website = req.body.website0;
console.log(website);
}
});
How it works: The controller finds the account, grabs an array from inside of it, sends it to the view. The view prints it out and allows to make some changes to the values.
And now is where the problems begin. I need to gather the new values into an array and send it to the next router, which will then use them to call a bunch of APIs and print out the data. How do I gather the array and pass it to the controller? Also, should I use GET or POST?
With your logic, the name attribute will have the following form: name=website[0...n]. With that in mind, we can filter out the keys to gather all the website[n] into an array you seek:
const example = {
website0: 'example',
website1: 'example',
website2: 'example',
website3: 'example',
shouldBeIgnored: 'ignoreMe',
ignore: 'shouldIgnore'
}
const websites = Object.keys(example).filter(key => key.startsWith('website'))
console.log(websites)
So you're controller can be:
router.get('/reports', (req, res, next) => {
if (!req.user.isPremium) {
// Free user - Single report
const builtWithCall = `https://api.builtwith.com/free1/api.json?KEY=00000000-0000-0000-0000-000000000000&LOOKUP=${website}`;
const websites = Object.keys(req.body).filter(key => key.startsWith('website'));
console.log(websites);
}
});

Expressjs Pagination Redirect

I have a pagination setup where my feed is displayed with the index route path / and then additional pages are accessed with /feed/:pageNumber. I have no issue with the pagination delivering the next and previous records, but when I want to have the ability on the last previousPage click to redirect to the / path since this has the most recent records. I tried to use res.redirect('/') on the else statement, but I get an error Error: Can't set headers after they are sent.. Is there a better approach to redirect to the home on the last previousPage click?
E.x. User is on '/', clicks Next and is sent to '/feed/2'. When User clicks Previous then they should be brought back to '/'. If they are on /feed/3,4,5,etc/ then it will bring the user to one less than the current parameter value.
Section I'm trying to fix:
if(req.params.pageNumber > 2){
res.locals.previous = true;
res.locals.previousPage = req.params.pageNumber - 1;
} else {
res.locals.previous = true;
res.locals.previous = res.redirect('/');
}
View:
<!DOCTYPE html>
<head>
{{> app/app-head}}
</head>
<body>
{{> app/app-navigation}}
<div class="container">
<h1 class="page-title-header">Activity Feed</h1>
{{> app/card}}
</div>
{{#if previous}}
Previous Page
{{/if}}
{{#if secondPage}}
Next Page
{{/if}}
{{#if next}}
Third Page
{{/if}}
</body>
Routes:
/*==== / ====*/
appRoutes.route('/')
.get(function(req, res){
models.Card.findAll({
order: 'cardDate DESC',
include: [{
model: models.User,
where: { organizationId: req.user.organizationId },
attributes: ['organizationId', 'userId']
}],
limit: 10
}).then(function(card){
function feedLength(count){
if (count >= 10){
return 2;
} else {
return null;
}
};
res.render('pages/app/high-level-activity-feed.hbs',{
card: card,
user: req.user,
secondPage: feedLength(card.length)
});
});
})
.post(function(req, res){
models.Card.create({
card: req.body.cardDate,
userId: req.user.userId
}).then(function() {
res.redirect('/app');
}).catch(function(error){
res.send(error);
})
});
appRoutes.route('/feed/:pageNumber')
.get(function(req, res){
function paginationPage(count){
if(count == 2){
return 10;
} else {
return (count - 1) * 10;
}
};
var skip = parseInt(req.params.pageNumber);
models.Card.findAll({
order: 'cardDate DESC',
include: [{
model: models.User,
where: { organizationId: req.user.organizationId },
attributes: ['organizationId', 'userId']
}],
offset: paginationPage(skip),
limit: 10
}).then(function(annotation){
if(annotation.length == 10){
res.locals.next = true;
res.locals.nextPage = parseInt(req.params.pageNumber) + 1;
console.log('This is the next page pagination: ' + res.locals.nextPage);
}
if(req.params.pageNumber > 2){
res.locals.previous = true;
res.locals.previousPage = req.params.pageNumber - 1;
} else {
res.locals.previous = true;
res.locals.previous = res.redirect('/');
}
res.render('pages/app/high-level-activity-feed.hbs',{
card: card,
user: req.user
});
});
})
The issue is two-fold:
your template always prefixes the previous page link with /feed/:
Previous Page
res.redirect('/') performs an actual redirect from within your Express server (it does not produce a link, or a client-side javascript)
A possible solution would be to add a third state to your template:
{{#if backHome}}
Previous Page
{{/if}}
And in your server code:
if (req.params.pageNumber > 2) {
res.locals.previous = true;
res.locals.previousPage = req.params.pageNumber - 1;
} else {
res.locals.backHome = true;
}

Resources