How can i save results from mongoose query to a variable - javascript

I'm trying to save some objects into an array by looping through a list of songs in an album, looking for relevant songs and trying to save into array for later use. is there any way to achieve this?
I need some explanation using mongoose.
exports.playlistPlayer = function (req, res, next) {
Playlist.findById({
_id: req.body.playlist._id
}, (err, playlist) => {
var customAlbum = []; //This variable it's inside the same block i believe
playlist.songs.forEach(function (song) {
Song.findById({
_id: song.song_id
}, (err, songs) => {
var customSong = {
title: songs.title,
time: songs.time,
source: songs.source,
song_id: songs._id
}
customAlbum.push(customSong)
console.log(customAlbum) //it works here
});
});
console.log(customAlbum) //it returns an empty array here where i need the data
});
};

The problem is that the findById method is also asynchronous. I recommend you to learn about promises in javascript. One possible solution would be using the async/await feature from ES7:
// asynchronous function
exports.playlistPlayer = async (req, res, next) => {
// wait for the findById method promise to resolve
const playlist = await Playlist.findById({
_id: req.body.playlist._id
})
// wait for finding all songs in db whose id's are in
// the playlist.songs array
const songs = await Song.find({
_id: { $in: playlist.songs }
})
// create the customAlbum by using the map method to
// tramsform the song objects to the required form
const customAlbum = songs.map(song => ({
title: song.title,
time: song.time,
source: song.source,
song_id: song._id
}))
// and there you should now have your customAlbum array
console.log(customAlbum)
// now you can use it for example
// to return a response to the client:
// res.json(customAlbum)
}

Related

Javascript for in loop not working for getting S3 pre-signed urls

I cannot get my code below to work.
I am querying for user object/s, then for each user object returned I need to get 2 pre-signed urls from its idKey and selfieKey property then send back the user objects with their respective pre-signed urls.
When trying to do this inside a for in loop I get [undefined, undefined] when logging the array keyArray so that code fails here before reaching the S3 method.
Any help would be greatly appreciated. Thank you
router.post("/api/verification/check", auth, async (req, res) => {
try {
const users = await User.find({ // Gets 1 or more User objects })
let usersWithUrls = [] // add results for each loop iteration
for (const user in users) {
const keyArray = [user.idKey, user.selfieKey]
console.log(keyArray)
const urlArray = await Promise.all(
keyArray.map((key) =>
S3.getSignedUrlPromise("getObject", {
Bucket: "app-bucket",
Key: key,
Expires: 30,
})
)
)
const idUrl = urlArray[0]
const selfieUrl = urlArray[1]
usersWithUrls.push({ user, idUrl, selfieUrl })
}
if (users) {
return res.send(usersWithUrls)
}
} catch (err) {
res.status(400).send()
}
}
)
Try changing your for in to a for of.
for (const user of users) {
for in loops gives you the index.
for of loops gives you the object

How do I return an array of documents using an array of users object ids in mongoose?

I am trying to create a simple back end blog api with user authentication and authorization. It is built with mongoose and express. In my userSchema, I have a property that is an array called "subscribedTo". Here, users can subscribe to different users to get their blogs. The subscribedTo array stores objectIDs of the users that wished to be subscribed too.
Here is my code:
router.get('/blogs', auth, async (req, res) => {
//auth middleware attaches user to the request obj
try {
let blogs = []
req.user.subscribedTo.forEach(async (id) => {
let ownersBlogs = await Blog.find({owner:id})
blogs = [...blogs, ...ownersBlogs]
console.log(blogs)//consoles desired output of users blogs
})
console.log(blogs)//runs first and returns []
res.send(blogs)
}catch(e){
res.status(500).send(e)
}
})
When I use postman for this route it returns [] which is understandable. I can't seem to res.send(blogs) even though the blogs variable returns correctly in the forEach function.
Is there a better way to do this?
You can use without loop like as bellow
Blog.find({ owner: { $in: req.user.subscribedTo } }, function (err, blogResult) {
if (err) {
response.send(err);
} else {
response.send(blogResult);
}
});
OR
send response after loop completed like as bellow
router.get('/blogs', auth, async (req, res) => {
//auth middleware attaches user to the request obj
try {
let blogs = []
let len = req.user.subscribedTo.length;
let i = 0;
if (len > 0) {
req.user.subscribedTo.forEach(async (id) => {
let ownersBlogs = await Blog.find({ owner: id })
blogs = [...blogs, ...ownersBlogs]
console.log(blogs)//consoles desired output of users blogs
i++;
if (i === len) {
//send response when loop reached at the end
res.send(blogs)
}
})
} else {
res.send(blogs);
}
} catch (e) {
res.status(500).send(e)
}
});
You can find all the documents without a foreach loop, use $in
Blog.find({owner:{$in:[array of ids]}});

Sequelize join on where condition returned from first table or return object values in an array derrived from foreach coming up empty

I've been trying to figure out this for a while now so any help would be very much appreciated.
I have one table called Interaction that searches with the client user's id and returns all interactions where they are the target user. Then I want to return the names of those users who initiated the interaction through the User table.
I tried using include to join the User table but I can't get the user's names using the where clause because it is based on a value returned in the first search of the Interaction table and don't know if I can search on a value that isn't the primary key or how?
The closest I've gotten is to use foreach and add the users to an array but I can't get the array to return in my response, because outside of the loop it is empty. I've tried suggestions I've found but can't figure out how to return the array outside of the foreach, if this is the best option. I am sure it is something really stupid on my behalf. TIA.
This is my attempt at include function:
getInvited: (req, res, next) => {
var user = {}
user = req.user;
let usrId = user[0]['facebookUserId'];
var userObjArray = [];
Interaction.findAll({
where: {
targetUserId: usrId,
status: 'invited',
},
include: [{
model: User,
attributes: [
'firstName'
],
where: {
facebookUserId: IwantToJoinOnInteraction.userId // replace with working code?
}]
}).then(function (users) {
res.send(users);
}).catch(next);
}
Or my attempt at foreach:
getInvited: (req, res, next) => {
var user = {}
user = req.user;
let usrId = user[0]['facebookUserId'];
var userObjArray = [];
Interaction.findAll({
where: {
targetUserId: usrId,
status: 'invited',
}
}).then(function (interactions) {
interactions.forEach((interaction) => {
User.findOne({
where: {
facebookUserId: interaction.userId // this is the where clause I don't know how to add in my first attempt with include
},
attributes: ['firstName', 'facebookUserId']
}).then(function (user) {
userObjArray.push(user['dataValues']);
console.log(userObjArray); // on the last loop it contains everything I need
})
})
res.status(200).send(userObjArray); // empty
}).catch(next);
},
You have to wait for all promises before sending the response. Your code runs async. With the forEach you are calling User.findOne async but you don't wait for all User.findOne to finish. A convenient way to make this work is Promise.all. You can pass an array of promises and the returned promise resolves to an array of all the resolved promises.
Promise.all(interactions.map(interaction => User.findOne(...)))
.then(users => {
res.status(200).send(users.map(user => user.dataValues))
})
You could write this much more easy to read woth async/await
getInvited: async (req, res, next) => {
...
const interactions = await Interaction.findAll(...)
const users = await Promise.all(interactions.map(interaction => User.findOne(...)))
res.status(200).send(users.map(user => user.dataValues))
}

Using Model.create() and save() inside for loop

Hey so I'm pretty new to Javascript and Node but I'm running into an issue that's been bothering me for a while.
I have a User model and an Image model, I'm using Multer to upload an array of images and trying to loop through this array, create a new Image model for each, then unshift that Image into my User's photos. I have Multer set up to successfully fills req.files. Here's the code.
router.post("/users/:user/photos/upload", middle.isLoggedIn, upload.array("photos", 4), function(req, res) {
User.findById(req.params.user, function(err, foundUser) {
for(var i = 0, len = req.files.length; i < len; i++) {
Image.create(req.files[i], function(err, newImage) {
if(err) {
return console.log(err.message);
}
newImage.human = foundUser;
newImage.save();
console.log(newImage);
foundUser.photos.unshift(newImage);
foundUser.save();
});
}
console.log(foundUser);
});
});
console.log(foundUser); seems to execute and print before console.log(newImage);
User Model
var mongoose = require("mongoose"),
passportLocalMongoose = require("passport-local-mongoose");
var UserSchema = new mongoose.Schema({
username: String,
password: String,
firstName: String,
lastName: String,
city: String,
photos: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Image"
}
]
});
HumanSchema.plugin(passportLocalMongoose);
module.exports = mongoose.model("User", UserSchema);
Image Model
var mongoose = require("mongoose");
var ImageSchema = new mongoose.Schema({
fieldname: String,
originalname: String,
mimetype: String,
filename: String,
destination: String,
size: Number,
path: String,
human: {
id: {
type: mongoose.Schema.Types.ObjectId,
ref: "Human"
}
}
});
module.exports = mongoose.model("Image", ImageSchema);
This is my first stackoverflow question so let me know if I didn't post enough code.
I think it have something to do with Image.create() being asynchronous, and I'm still trying to learn more about this and promises, but I still don't fully understand how it's relevant in my code.
Use Mongoose's promise support.
Promise.all allows you to resolve an array of promises.
async/await control flow for Promises.
I'm not sure your code as it is structured will work without a serial loop due the async code that can execute in any order. I'm not sure that triggering multiple foundUser.save()s on the same object at different times will work out very well. Holding database objects in memory for a long time can open up more concurrency data issues too.
The Bluebird promise library includes some additional helpers like Promise.each which will serially complete promises before the next starts which may be of use here.
const Promise = require('bluebird')
router.post("/users/:user/photos/upload", middle.isLoggedIn, upload.array("photos", 4), async function(req, res, next) {
try {
let foundUser = await User.findById(req.params.user)
await Promise.each(req.files, async file => {
let newImage = await Image.create(file)
newImage.human = foundUser;
await newImage.save()
console.log(newImage)
foundUser.photos.unshift(newImage)
await foundUser.save()
}
console.log(foundUser)
}
catch (err) {
next(err)
}
})
Other methods like .map and .reduce help make standard array/loop type operations with Promises easier to do.
Atomic Updates
In regards to the concurrency issue, any updates you can do in MongoDB that are "atomic" are a good thing. So instead of selecting something, modifying it in JS, then saving it back, you send the update to Mongo and let the db server deal with it. No matter what order you send the updates to the database, they will always be updating the latest copy of data.
In this case the array unshift can be completed without the initial select by using findByIdAndUpdate and $push (it can be made to push at position 0, there is no $unshift in mongo).
If you add a method to your User model for the adding the photo:
addPhoto(user_id, newImage){
return User.findByIdAndUpdate(
user_id,
{ $push: { photos: { $each: [newImage], $position: 0 } } } }
{ safe: true, new: true }
)
}
So the code would look like
const Promise = require('bluebird')
router.post("/users/:user/photos/upload", middle.isLoggedIn, upload.array("photos", 4), async function(req, res, next) {
try {
let foundUser = await User.findById(req.params.user)
if (!foundUser) throw new Error(`No user found: $user`)
let results = await Promise.map(req.files, async file => {
let newImage = await Image.create(file)
newImage.human = foundUser
await newImage.save()
console.log(newImage)
let user_update = await User.addPhoto(req.params.user, newImage)
console.log(user_update)
}
}
catch (err) {
next(err)
}
})

Execute Sequelize queries synchronously

I am building a website using Node.js and Sequelize (with a Postgres backend). I have a query that returns many objects with a foreign key, and I want to pass to the view a list of the objects that the foreign key references.
In the example, Attendances contains Hackathon keys, and I want to return a list of hackathons. Since the code is async, the following thing of course does not work in Node:
models.Attendance.findAll({
where: {
UserId: req.user.id
}
}).then(function (data) {
var hacks = [];
for (var d in data) {
models.Hackathon.findOne({
where: {
id: data[d].id
}
}).then(function (data1) {
hacks.append(data1);
});
}
res.render('dashboard/index.ejs', {title: 'My Hackathons', user: req.user, hacks: hacks});
});
Is there any way to do that query in a synchronous way, meaning that I don't return the view untill I have the "hacks" list filled with all the objects?
Thanks!
Use Promise.all to execute all of your queries then call the next function.
models.Attendance.findAll({
where: {
UserId: req.user.id
}
}).then(function (data) {
// get an array of the data keys, (not sure if you need to do this)
// it is unclear whether data is an object of users or an array. I assume
// it's an object as you used a `for in` loop
const keys = Object.keys(data)
// map the data keys to [Promise(query), Promise(query), {...}]
const hacks = keys.map((d) => {
return models.Hackathon.findOne({
where: {
id: data[d].id
}
})
})
// user Promise.all to resolve all of the promises asynchronously
Promise.all(hacks)
// this will be called once all promises have resolved so
// you can modify your data. it will be an array of the returned values
.then((users) => {
const [user1, user2, {...}] = users
res.render('dashboard/index.ejs', {
title: 'My Hackathons',
user: req.user,
hacks: users
});
})
});
The Sequelize library has the include parameter which merges models in one call. Adjust your where statement to bring the Hackathons model into Attendance. If this does not work, take the necessary time to setup Sequelize correctly, their documentation is constantly being improved. In the end, you'll save loads of time by reducing error and making your code readable for other programmers.
Look how much cleaner this can be...
models.Attendance.findAll({
include: [{
model: Hackathon,
as: 'hackathon'
},
where: {
UserId: req.user.id
}
}).then(function (data) {
// hackathon id
console.log(data.hackathon.id)
// attendance id
console.log(data.id)
})
Also..
Hackathon.belongsTo(Attendance)
Attendance.hasMany(Hackathon)
sequelize.sync().then(() => {
// this is where we continue ...
})
Learn more about Sequelize includes here:
http://docs.sequelizejs.com/en/latest/docs/models-usage/
Immediately invoke asynchronous function expression
This is one of the techniques mentioned at: How can I use async/await at the top level? Toplevel await is likely coming soon as of 2021, which will be even better.
Minimal runnable example:
const assert = require('assert');
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: 'db.sqlite',
});
const IntegerNames = sequelize.define(
'IntegerNames', {
value: { type: DataTypes.INTEGER, allowNull: false },
name: { type: DataTypes.STRING, },
}, {});
(async () => {
await IntegerNames.sync({force: true})
await IntegerNames.create({value: 2, name: 'two'});
await IntegerNames.create({value: 3, name: 'three'});
await IntegerNames.create({value: 5, name: 'five'});
// Fill array.
let integerNames = [];
integerNames.push(await IntegerNames.findOne({
where: {value: 2}
}));
integerNames.push(await IntegerNames.findOne({
where: {value: 3}
}));
// Use array.
assert(integerNames[0].name === 'two');
assert(integerNames[1].name === 'three');
await sequelize.close();
})();
Tested on Node v14.16.0, sequelize 6.6.2, seqlite3 5.0.2, Ubuntu 20.10.

Categories

Resources