How to get json data from multiple url-pages Node.js? - node.js

I am quite new to Node.js and haven't been working with json data before so really hope that you can help me.
I am trying to get all event information from Ticketmaster's API and add specific variables to mongoDB. However, the APIs' that I am currently using are limited to 200 events per page. It is therefore not possible for me to connect the event information with venue information since these are added seperately to mongoDB and are not exhaustive of all event and venue information (not able to connect on ids because of missing event and venue data).
My question is therefore in regards to how I can get all pages into my database at once?
The code that I have written so far looks something like below:
app.get('/tm', (req, res) => {
axios // getting venues
.get('https://app.ticketmaster.com/discovery/v2/venues.json?apikey=myApiKey&page=0&size=200&countryCode=DK')
.then(response => {
const venuesToBeInserted = response.data._embedded.venues.map(venue => { // preparing venues
return {
sourceID: venue.id,
venue: venue.name,
postalCode: venue.postalCode,
city: venue.city.name,
country: venue.country.name,
countryCode: venue.country.countryCode,
address: !!venue.address ? venue.address.line1 : null,
longitude: !!venue.location ? venue.location.longitude : null,
latitude: !!venue.location ? venue.location.latitude : null,
source: 'ticketmaster'
}
})
// Create all venues at once
Venue.create(venuesToBeInserted).then(venues => {
console.log("venues inserted")
axios // getting events and shows - note the page parameter in the api link
.get('https://app.ticketmaster.com/discovery/v2/events.json?apikey=myApiKey&countryCode=DK&size=200&page=0')
.then(response => {
const eventsToBeInserted = response.data._embedded.events.map(events => { // preparing events
const event = events._embedded.attractions[0]
return {
sourceID: event.id,
name: event.name,
slug: slugify(event.name).toLowerCase(),
tags: !!event.classifications ? [event.classifications[0].genre.name, event.classifications[0].subGenre.nam] : [], // duplicate genres occur
// possible tags from ticketmaster: type and subtype
}
})
// Create all events at once
Event.create(eventsToBeInserted).then(events => {
console.log("events inserted")
const showsToBeInserted = response.data._embedded.events.map(show => {
const event = events.find(event => event.sourceID == show._embedded.attractions[0].id);
const venue = venues.find(venue => venue.sourceID == show._embedded.venues[0].id);
if (!!event && !!venue) {
return {
event: event._id,
venue: venue._id,
timezone: show.dates.timezone,
dateStart: !!show.dates.start.dateTime ? show.dates.start.dateTime : show.dates.start.localDate,
tickets: !!show.priceRanges ? {
minPrice: show.priceRanges[0].min,
maxPrice: show.priceRanges[0].max,
currency: show.priceRanges[0].currency
}: {}
}
}
})
// Let's see what we have created in the database
Venue.find({}).select({
name: 1,
slug: -1
}).limit(10).populate('event').populate('venue').then(events => {
console.log(util.inspect(events));
}).catch(err => {
console.error(err);
});
}).catch( err => {
console.error(err)
})
}).catch( err => {
console.error(err)
})
}).catch(err => {
console.error(err)
});
}).catch(err => {
console.error(err)
})
})
EDIT
Using the approach that Jake suggested gave me an error (Error: Requested failed with status code 401). I have tried to search for it online but I cannot figure out why the error happens.. See picture below of part of the error message in my console.log.
error message

You can do this using promises, which you already are using, you just need to chain them together using recursion.
function getVenues(page, size, venues) {
page = page || 0;
size = size || 200;
venues = venues || [];
return axios
.get(`https://app.ticketmaster.com/discovery/v2/venues.json?apikey=myApiKey&page=${page}&size=${size}&countryCode=DK`)
.then(response => response.data._embedded.venues)
.then(rawVenues => {
rawVenues.forEach(venue => venues.push(venue));
if (rawVenues.length < size) {
// All done return the compiled list.
return venues;
}
// Recurse over the next set of venues by adding another promise to the chain.
return getVenues(page + 1, size, venues);
});
}
function getEvents(page, size, events) {
page = page || 0;
size = size || 200;
events = events || [];
return axios
.get(`https://app.ticketmaster.com/discovery/v2/events.json?apikey=myApiKey&countryCode=DK&size=${size}&page=${page}`)
.then(response => response.data._embedded.events)
.then(rawEvents => {
rawEvents.forEach(event => events.push(event));
if (rawEvents.length < size) {
// All done return the compiled list.
return events;
}
// Recurse over the next set of events by adding another promise to the chain.
return getEvents(page + 1, size, events);
});
}
app.get('/tm', (req, res) => {
getVenues().then(rawVenues => {
const venuesToBeInserted = rawVenues.map(venue => {
return {
sourceID: venue.id,
venue: venue.name,
postalCode: venue.postalCode,
city: venue.city.name,
country: venue.country.name,
countryCode: venue.country.countryCode,
address: !!venue.address ? venue.address.line1 : null,
longitude: !!venue.location ? venue.location.longitude : null,
latitude: !!venue.location ? venue.location.latitude : null,
source: 'ticketmaster'
};
});
// Return promise so errors bubble up the chain...
return Venue.create(venuesToBeInserted).then(venues => {
console.log("venues inserted");
// Return promise so errors bubble up the chain...
return getEvents().then(rawEvents => {
const eventsToBeInserted = rawEvents.map(rawEvent => {
const event = events._embedded.attractions[0];
return {
sourceID: event.id,
name: event.name,
slug: slugify(event.name).toLowerCase(),
tags: !!event.classifications ? [event.classifications[0].genre.name, event.classifications[0].subGenre.nam] : []
};
});
// Return promise so errors bubble up the chain...
return Event.create(eventsToBeInserted).then(events => {
console.log("events inserted");
const showsToBeInserted = rawEvents.map(show => {
const event = events.find(event => event.sourceID == show._embedded.attractions[0].id);
const venue = venues.find(venue => venue.sourceID == show._embedded.venues[0].id);
if (!!event && !!venue) {
return {
event: event._id,
venue: venue._id,
timezone: show.dates.timezone,
dateStart: !!show.dates.start.dateTime ? show.dates.start.dateTime : show.dates.start.localDate,
tickets: !!show.priceRanges ? {
minPrice: show.priceRanges[0].min,
maxPrice: show.priceRanges[0].max,
currency: show.priceRanges[0].currency
} : {}
}
}
});
// Do something with the found shows...
});
});
});
}).then(() => { // This then is fired after all of the promises above have resolved...
return Venue.find({}).select({
name: 1,
slug: -1
}).limit(10).populate('event').populate('venue').then(events => {
console.log(util.inspect(events));
res.send(events);
});
}).catch(err => { // Catches any error during execution.
console.error(err);
res.status(500).send(err);
});;
});

Related

node mongoose how to auto increment

Trying to follow the example here:
https://www.tutorialspoint.com/mongodb/mongodb_autoincrement_sequence.htm
export interface RowProps {
id?: number; // This is to auto increment
todoText: string;
}
const addAutoIncrement = async ({ db, collectionName, todoText }) => {
const getNextSequenceValue = (sequenceName: string) => {
const sequenceDocument = db
.collection<RowProps>(collectionName)
.findAndModify({
query: { _id: sequenceName },
update: { $inc: { sequence_value: 1 } },
new: true,
});
console.log('sequenceD', sequenceDocument)
return sequenceDocument.sequence_value;
};
db.collection<RowPropsClient>(collectionName).insertOne(
{
id: getNextSequenceValue('id'),
todoText
},
(err) => {
if (err) {
console.log("err");
}
}
);
}
// db is already defined and works
// I can add to the collection so this also works.
addAutoIncrement({ db, collectionName: 'todos', todoText: 'hello' });
Error: throw new Error('Collection#findAndModify unimplemented by driver');
^
Error: Collection#findAndModify unimplemented by driver
update
Tried to follow this example:
https://medium.com/#salonimalhotra1ind/how-to-increment-a-number-value-in-mongoose-785066ba09d8
const addAutoIncrement = async ({ db, collectionName, todoText }) => {
const modelTodo = db.model(collectionName, TodosSchema);
const res = await new modelTodo({ todoText }).save();
const { _id } = res;
return new Promise((resolve, reject) => {
modelTodo.findOneAndUpdate(
{ _id },
{ $inc: { id: 1 } },
{ new: true },
(err, res) => {
if (err) {
reject(err);
}
resolve(res);
}
);
});
};
**The result is just setting the value to 1 each time - not incrementing**
Collection#findAndModify() is a method that is implemented in the MongoDB shell, but not in the Node.js driver.
You should use Collection#findOneAndUpdate instead:
const { value : sequenceDocument } = db
.collection<RowProps>(collectionName)
.findOneAndUpdate({
{ _id: sequenceName },
{ $inc: { sequence_value: 1 } },
{ returnDocument : 'after' } // equivalent to `new: true`
});
ok I don't know why I didnt do this before. All the online examples make everything unnecessarily complicated.
Just get the total count and then add it.
const addAndIncrement = async ({ db, collection, todoText }) => {
const connectedModel = db.model(collection, TodosSchema);
const documentCount = await connectedModel.count({}); // get the total count
return new connectedModel({ todoText, id: documentCount }).save();
};
Unless anyone comes up with a more performant way, this is what I'm going with.

Firebase nodejs doesn't execute return function properly

We are trying to get timeslots from our database by pushing them into an array and then returning it. The array does get filled properly according to the firebase logs, however the function never returns the data properly at all, even though we see the data to be returned.
Basically, the execution does not reach the return statement.
Our goal is to get all of the timeslots in this photo. Is there any neat way to do this?
exports.getTimeslots = functions.region('europe-west2').https.onCall((data, context) => {
const uid = context.auth.uid;
let array = [];
if (!uid)
throw new functions.https.HttpsError('no-userid', 'The requested user was not found');
else
return admin.firestore().collection('users').doc(uid).collection('modules').where('name', '!=', '').get().then(snapshot => {
snapshot.forEach(async doc => {
await admin.firestore().collection('users').doc(uid).collection('modules').doc(doc.id).collection('timeslots').where('length', '!=', -1).get().then(snapshot2 => {
snapshot2.forEach(doc2 => {
array.push(Object.assign(doc2.data(), {id: doc2.id, modID: doc.id}))
console.log("identifier #1", array)
})
console.log("Got outside");
})
console.log("Got more outside");
})
console.log("Got the most outside")
return ({ data: array });
});
//console.log("I have escaped!")
})
As #Ragesh Ramesh also said: "The solution is to make everything async await.", I tried replicating your code using the data structure, and code below:
Data Structure:
Code:
// firebase db
const db = firebase.firestore();
exports.getTimeslots = functions.region('europe-west2').https.onCall((data, context) => {
const getData = async () => {
const uid = context.auth.uid;
let array = [];
if (!uid) {
throw new functions.https.HttpsError('no-userid', 'The requested user was not found');
} else {
const modulesRef = db.collection('users').doc(uid).collection('modules');
const modulesQuery = modulesRef.where('name', '!=', '');
const modulesQuerySnap = await modulesQuery.get();
const moduleDocuments = modulesQuerySnap.docs.map((doc) => ({ id: doc.id }));
for (const moduleDocument of moduleDocuments) {
const timeslotsRef = modulesRef.doc(moduleDocument.id).collection('timeslots');
const timeslotsQuery = timeslotsRef.where('length', '!=', -1);
const timeslotsQuerySnap = await timeslotsQuery.get();
const timeslotDocuments = timeslotsQuerySnap.docs.map((doc) => ({ id: doc.id, data: doc.data() }));
for (const timeslotDocument of timeslotDocuments) {
array.push(Object.assign(timeslotDocument.data, {id: timeslotDocument.id, modID: moduleDocument.id}))
}
}
return ({ data: array });
}
}
return getData()
.then((response) => {
// console.log(response);
return response;
});
}
The Reference page for Firestore reveals the docs property on the snapshot.
Upon running the code, here's the output:
{
data: [
{
length: 1,
id: '8UIlspnvelEkCUauZtWv',
modID: 'RmL5BWhKswEuMWytTIvZ'
},
{
title: 'Modules',
length: 120,
day: 1,
startTime: 720,
id: 'E5fjoGPyMswOeq8mDjz2',
modID: 'qa15lWTJMjkEvOU74N1j'
},
{
startTime: 360,
title: 'English',
day: 2,
length: 240,
id: '7JHtPSO83flO3nFOc0aE',
modID: 'qa15lWTJMjkEvOU74N1j'
}
]
}
This is a issue with how your function is written. Instead of
return ({ data: array });
Your function sometimes returns.
admin.firestore().collection('users').doc(uid).collection('modules').where('name', '!=', '').get()
Which is a promise by itself. You are chaining async inside then function. The solution is to make everything async await.
const data = await admin.firestore().collection('users').doc(uid).collection('modules').where('name', '!=', '').get()

How do I get useState to render each time I update the state?

I have a level up bar, where if you click the plus button it increases the bar by 10%.
I am using useEffect to grab the skills of a specific user by filtering what I get back from mongoDB(Mongoose).
I created a refreshHandler callback function that I calling to increment refresh, so that useEffect would make a request again to my back-end and I was hoping it would render the component again.
It does this once at start, then one more time and then stops rendering. (The changes are still happening in the Database, although the level only goes up one time when clicked)
App.js
function App() {
// States
const [inputText, setInputText] = useState('');
const [skills, setSkills] = useState([]);
const [session, setSession] = useState({ auth: false, token: '', user_id: '', refresh: 0 });
var { isLoggedIn, refresh } = session;
const userAuth = (data) => {
setSession({
auth: true,
token: data.token,
user_id: data.user_id,
});
console.log('User logged in successfully!');
};
const refreshHandler = () => {
setSession({ ...session, refresh: session.refresh + 1 });
};
useEffect(() => {
console.log('USE EFFECT MOUNTED!!');
const fetchSkills = async () => {
const response = await axios.get('http://localhost:3001/api/users/fetchSkills');
console.log('response: ', response.data);
setSkills(
response.data.filter((skill) => {
if (skill.user_id == session.user_id) {
return skill;
}
})
);
};
fetchSkills();
}, [refresh]);
This is the skill.js component that I am using in my skillList component to show a list of skills.
skill.js
const Skill = ({ refreshHandler, text, level, skills, skill, setSkills, percent }) => {
const levelUpHandler = async () => {
let data = { _id: skill._id, level: skill.level, percent: skill.percent };
if (skill.percent == 90) {
data.percent = 0;
data.level += 1;
} else {
data.percent += 10;
}
const response = await axios
.put('http://localhost:3001/api/users/update-skill', data)
.then(console.log(data))
.catch((err) => console.log(err));
refreshHandler();
};
I tried using an example of useRef that I found but that ended up rendering/making the axios request in a loop.
Node.js backend - route
router.put('/update-skill', function (req, res) {
var id = req.body._id;
var update = { level: req.body.level, percent: req.body.percent };
console.log(update);
Skill.findByIdAndUpdate(id, update, { new: true }, (err, res) => {
if (err) {
console.log(err);
} else {
console.log('Success:', res);
}
});
});
MAIN GOAL:
Each time button is clicked, update the percent/level of skill in the back-end(mongoose/ mongoDB Atlas - This is working but only one request per skill, the skill goes from 0 to 10% then each consecutive click console.logs() 10% instead of going up).
Would like to see the progress bar increase each time button is clicked.
Any help would be greatly appreciated and as if more info is needed for the problem to be clear please let me know.
I figured out the solution to my problem!
Basically when I was making a put request using axios to my backend, the request was being sent but no response was given from the backend.
Diagnosed by checking the network tab and it had a pending for the request. It seems after 5 pending request the database stalls.
So to fix it I added a return statement.
router.put("/update-skill", async (req, res) => {
var id = req.body._id;
var update = { level: req.body.level, percent: req.body.percent };
try {
var responseData = await Skill.findByIdAndUpdate(id, update, { new:
true });
} catch (err) {
return res
.status(500)
.json({ error: "skill was not updated", message: err });
}
// This was the missing code I needed!
return res.json({ response: "Successful", data: responseData });
});

Mongoose exec() does not await properly

For some reason, exec() is not awaiting in my code:
let team = <SOME TEAM NAME> //putting manually for testing
let results = []
TeamModel.find({ name: team })
.exec(async (err, docs) => {
if (err)
return res.send(Response("failure", "Error occured while retrieving fixtures"))
for(let i = 0; i < docs.length; i++){
let doesExist = await FixtureModel.exists({ leagueName: docs[i].leagueName })
if (doesExist) {
let query = FixtureModel.find({ leagueName: docs[i].leagueName, $or: [{ homeTeam: team }, { awayTeam: team }] })
await query.exec((err2, docs2) => {
if (err2)
return res.send(Response("failure", "Error occured while retrieving fixtures"))
docs2.forEach((doc2, index) => {results.push(doc2.toObject())})
console.log('during await') //Executes second
})
console.log('after await') //Executes first
}
else { //This section is not required
let result = await Communicator.GetFixturesFromLeague(docs[i].leagueId)
result.api.fixtures.forEach((fixture, index) => {
let newFixture = new FixtureModel({
fixtureId: fixture.fixture_id,
leagueId: fixture.league_id,
leagueName: fixture.league.name,
eventDate: fixture.event_date,
statusShort: fixture.statusShort,
homeTeam: fixture.homeTeam.team_name,
awayTeam: fixture.awayTeam.team_name
})
newFixture.save()
results.push(newFixture.toObject())
})
}
}
console.log(results)
res.send(Response("success", "Retrieved fixtures", results))
})
and the result looks something like this:
after await
[]
during await
and therefore an empty array of results is sent before values are added inside. I'm not sure what I'm missing here.
This is a workaround if anyones wondering. Sending the response inside the exec() callback when the forLoop completes bypassing the need for async/await.
if (doesExist) {
let query = FixtureModel.find({ league_name: docs[i].leagueName, $or: [{ homeTeam: { team_name: team} }, { awayTeam: { team_name: team} }] })
await query.exec((err2, docs2) => {
if (err2)
return res.send(Response("failure", "Error occured while retrieving fixtures"))
docs2.forEach((doc2, index) => {results.push(doc2.toObject())})
if(i + 1 == docs.length){
return res.send(Response("success", "Retrieved fixtures", results))
}
})
}

How to implement async in for loop?

I have a collection called 'alldetails' which have the details of some collection
{
"name" : "Test1",
"table_name" : "collection1",
"column_name" : "column1"
},
{
"name" : "Test2",
"table_name" : "collection2",
"column_name" : "column2"
},
{
"name" : "Test3",
"table_name" : "collection3",
"column_name" : "column3"
}
I have collection1,collection2 and collection3 which have column1,column2,colum3 respectively
I have to fetch all the name from the 'alldetails' and I have to get the min and max value of other table based on the column name.
So I want the output like below
{name: ["Test1","Test2","Test3"],
date: [{min_date: "2018-12-01", max_date: "2018-12-31", name: "Test1"},
{min_date: "2018-12-01", max_date: "2018-12-31", name: "Test2"},
{min_date: "2018-12-01", max_date: "2018-12-31", name: "Test3"}]
}
I tried the below code because of non blocking its not waiting the response.
alldetails.find({}, { _id: 0 }).then(async function(result) {
let result_data = {};
let resolvedFinalArray = {};
let array = [];
result_data["name"]= [];
result_data["date"] = [];
resolvedFinalArray = await Promise.all(result.map(async value => {
result_data["name"].push(value.name)
getResult(value.table_name,value.column_name,function(response){
result_data["date"].push({min_date: response.minvalue, max_date: response.maxvalue, name:value.name})
});
}));
setTimeout(function()
{
console.log(resolvedFinalArray);
}, 3000);
});
Please suggest me a solution.
If you want to wait for getResult then you need to return Promise from result.map callback.
You are not pushing anything to resolvedFinalArray so why bother with console.log(resolvedFinalArray)
alldetails.find({}, {_id: 0}).then(async (result) => {
let result_data = {};
result_data["name"] = [];
result_data["date"] = [];
await Promise.all(result.map(value => {
// create Promise that resolves where getResult callback is fired
return new Promise((resolve) => {
getResult(value.table_name, value.column_name, (response) => {
result_data["name"].push(value.name);
result_data["date"].push({
min_date: response.minvalue,
max_date: response.maxvalue,
name: value.name
});
resolve();
});
});
}));
console.log(result_data);
});
or using for loop
alldetails.find({}, {_id: 0}).then(async (result) => {
let result_data = {};
result_data["name"] = [];
result_data["date"] = [];
for (let i = 0; i < result.length; i++) {
const value = result[i];
await new Promise((resolve) => {
getResult(value.table_name, value.column_name, (response) => {
result_data["name"].push(value.name);
result_data["date"].push({
min_date: response.minvalue,
max_date: response.maxvalue,
name: value.name
});
resolve();
});
});
}
console.log(result_data);
});
use async.eachOfLimit if you want to apply an async function on all element of an array:
var async = require("async");
var array = [{_id: "...."},{...},{...}];
async.eachOfLimit(array, 1, function(element, index, cb){
myAsyncFunctionWithMyElement(element, function(err){
return cb(err);
});
}, function(err){
// final callback
});
The array forEach method won't work with async function (unless you do deeply evil things like redefining the prototype). This question has a nice insight of the internal.
If you don't want to rely on external libraries, an easy (and my favourite) approach is something like:
for (let i = 0; i < <your array>.length; i++ ) {
await Promise.all( <your logic> );
}
Just adapt it to your need! :)
You might want to use the for await of loop. See this blog post for details.
This, IMHO, is the most modern way to do it, and it doesn't require you to load any external dependencies, since it is built-in to the language itself. It's basically very similar to the classical for of loop.
This should work, if all lexical scope are taken to consideration. Async each is also is better option it would reduce if else blocks and manage promise for you.
alldetails.find({}, { _id: 0 })
.exec((err, result) => {
if (!err) {
let resolvedFinalArray = [];
result.map((value) => {
resolvedFinalArray.push({
name: value.name,
date: []
});
getResult(value.table_name, value.column_name, (err, response) => {
if (!err) {
resolvedFinalArray[resolvedFinalArray.indexOf(value.name)]['date'].push({
min_date: response.minvalue,
max_date: response.maxvalue,
name:value.name
});
} else {
// Send your error messsage.
// res.status(500).send(err);
}
});
});
console.log(resolvedFinalArray);
// res.send(resolvedFinalArray);
} else {
// Send your error messsage.
// res.status(500).send(err);
}
});

Resources