I'm very new with Node, KnexJS and promises and I'm trying to build a simple loop that queries items and then adds the pictures associated with them.
I looked at this answer and while it teaches a few things I don't think it works for my case: Knex Transaction with Promises
So far I have this:
router.get('/', function(req, res, next) {
knex('parts').where(req.query)
.orderBy('date_updated', 'DESC')
.then(function(data){
for (var k in data) {
knex('photos')
.select()
.where('part_id', data[k].id)
.then(function(photos){
data[k].photos = photos;
});
}
return data;
})
.then(function(data){
res.render('parts/index', { title: 'Express', data: data, query: req.query });
});
});
Which is obviously wrong, but I just don't know the approach in these cases.
Adding to IvanSF's answer, you can simply wrap a Promise.all() around that, to then res.send() the response. Like so:
Promise.all(rows.map(row => {
return knex('table')
.select('*').where('row_id', row.id)
.then(table => {
row.table = table;
return row;
});
})).then(response => {
res.send(response);
});
I used .map to get the desired effect.
.map(function(row) {
return knex('photos')
.select()
.where('part_id', row.id)
.then(function(photos) {
row.photos = photos;
return row;
});
})
Related
I'm currently trying to send 2 objects to the front .hbs front end. However I cant seem to work out how to do this because I'm using promises.
Currently, my thinking is i perform the sql query, the country and organisation name is extracted, and then each sent to a geocoding api, returned and then squashed together in the same promises. But i'm not sure how to extract these for the render function.
Node
//route for homepage
app.get('/', (req, res) => {
let sql = "SELECT org_name, country_name from places;
let query = conn.query(sql, (err, results) => {
if (err) throw err;
const geoPromise = param => new Promise((resolve, reject) => {
geo.geocode('mapbox.places', param, function(err, geoData) {
if (err) return reject(err);
if (geoData) {
resolve(geoData.features[0])
} else {
reject('No result found');
}
});
});
const promises = results.map(result =>
Promise.all([
geoPromise(result.country_name),
geoPromise(result.org_name)
]));
Promise.all(promises).then((geoLoc, geoBus) => {
res.render('layouts/layout', {
results: JSON.stringify(geoLoc),
businesses: JSON.stringify(geoBus)
});
});
});
});
Front end call
results1 = {{{results}}}
console.log(results1.length)
business1 = {{{businesses}}}
console.log(business1.length)
Wrap your geo.geocode into a Promise
const geoPromise = param => new Promise((resolve, reject) => {
geo.geocode('mapbox.places', param, function(err, geoData) {
if (err) return reject(err);
if (geoData) {
resolve(geoData.features[0])
} else {
reject('No result found');
}
});
});
Combine both calls to geo.geocode
const promises = results.map(result =>
Promise.all([
geoPromise(result.country_name),
geoPromise(result.org_name)
]));
Call them
Promise.all(promises).then(([geoLoc, geoBus]) => {
res.render('layouts/layout', {
results: JSON.stringify(geoLoc),
businesses: JSON.stringify(geoBus)
});
});
As MadWard's answer mentions, deconstructing the argument of the callback of Promise.all is necessary since everything will be in the first argument. Make sure you check out his post for more details
Something important to recall: you will never have more than one argument in a then() callback.
Now you may ask: in the case of Promise.all(), what is this value?
Well, it is an array with all the values from the promises it awaits, in the order in which they are called.
If you do:
Promise.all([
resolveVariable1, resolveVariable2, resolveVariable3
]).then((values) => {
})
values will be [variable1, variable2, variable3], the three variables that the promises resolve to.
Your case is, however, a bit more complicated. What is gonna be returned at the end is a 2-D array containing every entry. It is an array of length results.length, and each of its element has a length of 2. The first element is the result, and the second one is the business.
Here is your snippet:
Promise.all(promises)
.then((values) => {
let results = values.map(elmt => elmt[0]);
let businesses = values.map(elmt => elmt[1]);
res.render('layouts/layout', {
results: JSON.stringify(results),
businesses: JSON.stringify(businesses)
});
})
I have a fairly straightforward CRUD app which renders the results of two queries onto one page. The problem that arose once I got this to "work" was that the page required a refresh in order to display the results. On first load, no results were displayed.
I came to figure out that this is a problem/symptom of Node's asynchronous nature. I've been trying to approach this problem by using async/await, and from hours of messing with things, I feel like I'm quite close to the solution, but it's just not working out - I still need a manual refresh to display/render the results on the .ejs page.
The code:
var entries = [];
var frontPageGoals = [];
app.get('/entries', async (req,res) => {
if (req.session.password) {
const entriesColl = await
db.collection('entries')
.find()
.sort({date: -1})
.toArray((err, result) => {
if (err) { console.log(err) }
else {
for (i=0; i<result.length; i++) {
entries[i] = result[i];
}
}
});
const goalsColl = await
db.collection('goals')
.find()
.toArray((err, result) => {
if (err) {console.log(err)}
else {
for (i=0; i<result.length; i++) {
frontPageGoals[i] = result[i];
}
}
});
res.render('index.ejs', {entries: entries, frontPageGoals: frontPageGoals});
}
else {
res.redirect('/');
}
});
Now, I can conceive of a few problems here, but honestly I'm just at my wits end trying to figure this out. For example, I'm sure it's problematic that the empty lists which will contain the results to be passed when the page renders are outside the actual async function. But after trying to move them a dozen different places within the async area... still no dice.
Any help would be hugely appreciated! This is basically the last big "thing" I need done for this app.
I'm not 100% sure about your database driver, but assuming that the toArray() returns a promise (which it does in the default mongodb driver), the await will actually return the value you expect in your callback, result in your case, or in case there was an error, which you expected it as err in your callback, it will be thrown, thus forcing you to use try-catch blocks, in your case, you would just use console.log(err) in the catch block, since you aren't doing any handling
Here's your code after updating :
app.get("/entries", async (req, res) => {
if (req.session.password) {
try {
const entries = await db
.collection("entries")
.find()
.sort({ date: -1 })
.toArray();
const frontPageGoals = await db
.collection("goals")
.find()
.toArray();
res.render("index.ejs", {
entries: entries,
frontPageGoals: frontPageGoals
});
} catch (err) {
console.log(err);
}
} else {
res.redirect("/");
}
});
EDIT
However, if you don't know about promises -which async/await are basically promises-, and wanna just do it using callbacks -not advised-, you would have to just send your response in the callback, and nest the 2nd query in the first query's callback, here is the code,, with some comments to hopefully help you out:
app.get("/entries", (req, res) => {
if (req.session.password) {
// First query
db.collection("entries")
.find()
.sort({ date: -1 })
.toArray((err, entryResult) => {
if (err) {
console.log(err);
} else {
// In the callback of the first query, so it will
// execute 2nd query, only when the first one is done
db.collection("goals")
.find()
.toArray((err, frontPageResult) => {
if (err) {
console.log(err);
} else {
// In the callback of the 2nd query, send the response
// here since both data are at hand
res.render("index.ejs", {
entries: entryResult,
frontPageGoals: frontPageResult
});
}
});
}
});
} else {
res.redirect("/");
}
});
I have removed the async keyword since you no longer need it
I renamed the callback arguments, instead of just result, because both callbacks would have the same argument name, and you would have had to store it in a temp variable
In my NodeJS application, I am building an API that first fetches all tarrifs name from tarrifs collection then based on all those tarrifs I want to return the counts of these tariffs allocated to users I tried the below-given code
router.get('/getTarrifDetails', (req,res,next) => {
result=[];
tname=[];
counts=[];
Tarrif.find().distinct('tarrif_type', (err,docs) => {
docs.forEach((ele) => {
tname.push(ele);
User.countDocuments({tarrif_type:ele}, (uerr,usr) => {
counts.push(usr);
});
result.push(ele);
});
result.push(counts);
});
});
When I console.log(result) it only shows one array of tarrif_type and other array is empty
You need to understand how the event loop works. I was here once, and I made the same mistakes once.
Try to sync your callbacks since you want to sequentially, like so:
router.get('/getTarrifDetails', (req, res, next) => {
let result = [], count = 0;
Tarrif.find().distinct('tarrif_type', (err, docs) => {
async.forEach(docs, async ele => {
try {
let userCount = await User.countDocuments({ tarrif_type: ele });
result.push(userCount);
} catch (err) {
//your err goes here.
}
})
});
});
I am not sure this will work 100%, but try it out and debug a little bit.
I am using twitter's API to receive recent tweets by querying specific hash tags. The first GET request is to search/tweets which takes the hashtag and others queries in it's parameters. The response for this returns an array of tweets (objects). I push each of these id's into an external array. Now I want to loop through this array and make another call to statuses/show for each of these IDs so that I can get the location data of the user posting the tweet. The statuses/show end-point only takes a single tweet id but I have an array. How would I go about using the same getRequest function for an array of IDs?
I tried to implement it by reading online about Promises but it's not working.
Here's my code:
function getRequest(url, params) {
return new Promise(function (success, failure) {
twitterSearch.get(url, params, function (error, body, response) {
if (!error) {
success(body);
} else {
failure(error);
}
});
});
}
app.post('/twitter', (req, res) => {
console.log("body", req.body);
var tweetIDs = [];
var bounds = [];
getRequest('search/tweets', {q: req.body.tag, result_type:'recent', count:'100'})
.then((tweets) => {
tweets.statuses.map((status) => {
if(status.user.geo_enabled) {
console.log("ID: ", status.id);
tweetIDs.push(status.id);
}
});
return getRequest('statuses/show', tweetIDs); //I know tweetIDs is wrong, dont know what to do from here.
}).then((data) => {
console.log("User data for the ID")
console.log(data);
}).catch((error) => {
console.log(error)
});
res.send(bounds);
bounds.length = 0;
});
You nearly got it right! Map all tweets to Promises returned by your getRequest() function. Then, the Promise.all() method will return you a single Promise that resolves when all of the promises in your array resolves.
By the way, you can use Array.reduce instead of map to both filter and map your array at the same time, this factor your code a little better.
getRequest('search/tweets', {q: req.body.tag, result_type:'recent', count:'100'})
.then( tweets => {
let requests = tweets.statuses.reduce( (ret,status) => {
if(status.user.geo_enabled)
// The following is still wrong though, I guess. Format the params of getRequest accordingly.
ret.push( getRequest('statuses/show', status.id) );
return ret;
}, []);
return Promise.all(requests);
})
.then( results => {
results.forEach( res => {
console.log("User data for the ID");
console.log(res);
})
})
EDIT : Related jsfiddle
Replace the line
return getRequest('statuses/show', tweetIDs); //I know tweetIDs is wrong, dont know what to do from here.
with
return Promisel.all( tweetIDs.map( (tId) => getRequest('statuses/show', tId) );
I am quite new to Node.js and already frustrated due to nested callbacks which make it very hard to read the code and troubleshoot for typos.
As you can see below, I have 2 associated models (Blog and Comment) and app.get method which I create Comment for a Blog post.
Model Structure:
Blog
..title (string)
..blog (string)
..comments (Referenced Comment Model)
....comment (string)
Comment
..comment(string)
Currently app.get method has 3 nested call back functions, possible errors are only console.logged yet (for a better user experience if I start to write more codes for errors function becomes real mess).
app.post('/blog/:id/comment',function(req,res){
Comment.create(req.body.comment, function(err, newComment){
if (err) {
console.log(err);
} else {
Blog.findById(req.params.id, function(err, foundBlog){
if (err){
console.log(err);
} else {
foundBlog.comments.push(newComment);
foundBlog.save(function(err, data){
if(err){
console.log(err);
} else {
res.redirect('/blog/'+req.params.id);
}
});
}
});
}
});
});
Here I would like to ask your suggestions to simplify below function and how to better handling errors.
As others have commented, promises is the way to go and async/await is generally the most elegant approach to writing promises. As an example, your code could be condensed to the below. You should read up on promises as they are an important concept for node development.
app.post('/blog/:id/comment', async function(req,res){
try{
const newComment = await Comment.create(req.body.comment);
const foundBlog = await Blog.findById(req.params.id);
foundBlog.comments.push(newComment);
await foundBlog.save();
res.redirect('/blog/'+req.params.id);
}
catch(err){
console.log(err);
}
});
Looks like you are using Mongoose, which supports promises, so you could do something like this:
app.post('/blog/:id/comment',(req,res) {
Comment.create(req.body.comment)
.then(newComment => {
return Blog.findById(req.params.id))
.then(foundBlog => {
foundBlog.comments.push(newComment)
return foundBlog.save()
})
})
.then(() => res.redirect('/blog/' + req.params.id))
.catch(err => console.log(err))
})
You could also use async-await:
app.post('/blog/:id/comment', async (req, res) {
try {
const newComment = await Comment.create(req.body.comment)
const foundBlog = await Blog.findById(req.params.id)
foundBlog.comments.push(newComment)
await foundBlog.save()
res.redirect('/blog/' + req.params.id)
}
catch(err) {
console.log(err)
}
})