Node.js / Express / mongoose backend - async operations within array.map() - javascript

I am building my first solo project for a paper music magazine to offer their subscribers online reading. I already know that I will have to refactor a lot of duplicate into reusable code but I just want things to work for now.
These are the collections I use to build the models/controllers:
Artists
Albums
Users (admins, authors, subscribers)
Chronicles (album short reviews)
Articles (album(s) long reviews)
Interviews (related to articles most of the times)
Issues, referencing all Chronicles, Articles and Interviews (ObjectID arrays)
Artist is straightforward and simple, just 'name', 'country' and an empty 'albums' array:
exports.createArtist = catchAsync(async (req, res, next) => {
req.body.createdBy = mongoose.Types.ObjectId(req.user._id)
req.body.albums = []
const newArtist = await Artist.create(req.body)
res.status(201).json({
status: 'success',
data: {
article: newArtist,
},
})
})
Album is a bit more complex, but the artist 'name' is transformed into its ObjectId for referencing, and populates the artist's 'albums' array with the newly created album ObjectId
exports.createAlbum = catchAsync(async (req, res, next) => {
// Add createdBy automatically in the req.body
req.body.createdBy = mongoose.Types.ObjectId(req.user._id)
// Find the album's artist by its name
const relatedArtist = await Artist.findOne({ name: req.body.artist })
if (!relatedArtist) {
return next(new AppError('No artist with that name, please check', 404))
}
// replace the name of the artist with its objectID for auto referencing
req.body.artist = mongoose.Types.ObjectId(relatedArtist._id)
// save album
const newAlbum = await Album.create(req.body)
// add the saved album into the albums' array in the artist collection
relatedArtist.albums.push(mongoose.Types.ObjectId(newAlbum._id))
await relatedArtist.save()
res.status(201).json({
status: 'success',
data: {
album: newAlbum,
},
})
})
Chronicle is a bit similar but involves 3 other collections in the process: collection of album & artist data, and populating the 'chronicles' array of the current issue:
exports.createChronicle = catchAsync(async (req, res, next) => {
// Add author automatically in the req.body
req.body.author = mongoose.Types.ObjectId(req.user._id)
// Find artist & album of the chronicle in the respective collections
const artist = await Artist.findOne({ name: req.body.artist })
const album = await Album.findOne({ title: req.body.album })
const issue = await Issue.findOne({ issueNumber: req.body.belongsToIssue })
if (!artist) {
return next(new AppError('No artist with that name, please check.', 404))
}
if (!album) {
return next(new AppError('No album with that name, please check.', 404))
}
if (!issue) {
return next(new AppError('No issue with that number, please check.', 404))
}
// replace the name of the artist and album with its objectID for auto referencing
req.body.artist = mongoose.Types.ObjectId(artist._id)
req.body.album = mongoose.Types.ObjectId(album._id)
// Create Chronicle unique slug & add to req.body
req.body.slug = slugify(`${artist.name} ${album.title} ${album.year}`, {
lower: true,
})
// Save new Chronicle
const newChronicle = await Chronicle.create(req.body)
// Push new Chronicle ID into the array of the corresponding Issue
issue.chronicles.push(mongoose.Types.ObjectId(newChronicle._id))
await issue.save()
res.status(201).json({
status: 'success',
data: {
chronicle: newChronicle,
},
})
})
My problem comes now for 'Articles' :
An article can be about several albums (so not just an ObjectId, but an array of ObjectIds!) and can be signed by several writers (between 1 and 3). I have to loop through both arrays while performing each time:
await Album.find({title: req.body.title})
await User.find({author: req.body.author})
then swap the names in the req.body.albums & req.body.authors by its ObjectId and in the end, transforming the req.body.albums + authors from an array of strings into an array of ObjectIds, especially where arrays are pointers (I'm guessing I have to work with a destructured duplicate array).
I understood that I can't perform async operations within a forEach() of map() loop but haven't figured out how to make this work. My research makes me think I have to use Promise.all() but haven't figured out how to so far, all my trials and errors failed until now, so I must do this the wrong way or don't understand the process.
Thank you for the help and tricks!

Thank you Anatoly. Your tip led me to something that needs to be optimized and refactored but that has the huge advantage of being totally functional:
exports.createArticle = catchAsync(async (req, res, next) => {
// Add authors automatically in the req.body if not specified by user
if (!req.body.authors || req.body.authors === []) {
req.body.authors.push(mongoose.Types.ObjectId(req.user._id))
}
// loop through all authors names and swap with respective ObjectIds
const tempAuthors = []
await Promise.all(
req.body.authors.map(async (author, index) => {
const user = await User.findOne({ name: author })
if (!user) {
return next(
new AppError(
`No author with that name (position ${index + 1}), please check.`,
404
)
)
}
tempAuthors.push(mongoose.Types.ObjectId(user._id))
})
)
// Assign req.body.authors the values of tempAuthors
req.body.authors = [...tempAuthors]
// loop through all album titles and swap with respective ObjectIds
const tempAlbums = []
await Promise.all(
req.body.albums.map(async (title, index) => {
const album = await Album.findOne({ title })
if (!album) {
return next(
new AppError(
`No album with that title (position ${index + 1}), please check.`,
404
)
)
}
tempAlbums.push(mongoose.Types.ObjectId(album._id))
})
)
// Assign req.body.authors the values of tempAuthors
req.body.albums = [...tempAlbums]
// Find artist & issue of the article
const artist = await Artist.findOne({ name: req.body.artist })
if (!artist) {
return next(new AppError('No artist with that name, please check.', 404))
}
const issue = await Issue.findOne({ issueNumber: req.body.belongsToIssue })
if (!issue) {
return next(new AppError('No issue with that number, please check.', 404))
}
// replace the name of the artist with its objectID for auto referencing
req.body.artist = mongoose.Types.ObjectId(artist._id)
// Create Article unique slug & add to req.body
req.body.slug = slugify(
`${issue.issueNumber} ${artist.name} ${req.body.title}`,
{
lower: true,
}
)
// Save new Article
const newArticle = await Article.create(req.body)
// Push new Article ID into the array of the corresponding Issue
issue.articles.push(mongoose.Types.ObjectId(newArticle._id))
await issue.save()
res.status(201).json({
status: 'success',
data: {
article: newArticle,
},
})
})
The next step will be to outsource the swap function for Albums and Authors to avoid duplicate code.

Related

Express js related articles - error cannot read properties of null

I try to show related articles based on categories. I use mongoose to query things from MongoDB.
When I try the code below, I get the error "TypeError: Cannot read properties of null (reading 'category')" but console.log from articlecategories first showing array with categories and then throw the error "cannot read"..
I'm a beginner in express js, maybe someone gives me a hint.
exports.articleDetail = async (req, res) => {
const article = await Article.findOne({ slug: req.params.slug }).populate('category').populate('author');
const articlecategories = article.category
categories = []
for(let i=0;i<articlecategories.length;i++){
const category = articlecategories[i]
categories.push(category._id)
}
console.log(categories)
const relatedarticles = await Article.find({ category : { $all : categories }})
console.log(article);
res.render('article', { article, relatedarticles })
}
Edit
Thank You all for answers. I have a solution. Problem is that when loop through article categories, I get no category ID but new ObjectId: new ObjectId("636bc1c64f7470f2557b61d7")
To let this work, I must use .toString() and get only Id and then push this Id to array.
This is working code:
exports.articleDetail = async (req, res) => {
const article = await Article.findOne({ slug: req.params.slug }).populate('category').populate('author');
categories = []
for(let i=0;i<article.category.length;i++){
const category = article.category[i]
const catid = category._id.toString()
categories.push(catid)
}
console.log(categories)
const articles = await Article.find({category: { $all: categories }}).populate('author')
res.render('article', { article, articles })
}
Probably your call to await Article.findOne just returns null since the object could not be found.
You should check if there is anything found and, if not, directly return an error like this:
exports.articleDetail = async (req, res) => {
const article = await Article.findOne({ slug: req.params.slug }).populate('category').populate('author');
if ( !article ) return res.status(404).json(/* include your error object here */);
const articlecategories = article.category
categories = []
for(let i=0;i<articlecategories.length;i++){
const category = articlecategories[i]
categories.push(category._id)
}
console.log(categories)
const relatedarticles = await Article.find({ category : { $all : categories }})
console.log(article);
res.render('article', { article, relatedarticles })
}

How to use the findOne mongoDB query correctly?

My goal is to get the one product object back depending on the product id i marked in green (out of the whole products array in my mongo DB)
My Backend entry looks as follows:
router.get("/:id", async (req, res)=> {
const mid=req.params.id;
console.log(mid)
const products = await Product.findOne({ id: mid })
console.log(products)
if (products) {
res.send(products);
} else {
res.status(404).send({message:"product not found"})
}
});
Connsole.log(mid) on line three works and it gives the right id back. However when i try to filter that one array depending on the value in line three i always get back the first object of my Database, which is the gopro camera, instead of the right object.
The Output looks as Follows:
632834528
{
_id: '5f9849daf641a82b257d529b',
id: 3484,
agentId: 66343,
title: 'GoPro Camera',
slug: 'gopro',
What am i doing Wrong?
I tried const products = await Product.find({ id: mid }) as well, but it gives me the whole array back instead of just the one object.
I think it's returning a Query. Try:
const products = await Product.findOne({ id: mid }).exec();
This solution worked for me:
I have to use the Expressasynchandler like this:
Edit: This solution might not be the best (see comments on answer above)
router.get("/:id", expressAsyncHandler(async (req, res) => {
const mid=req.params.id;
const products = await Product.findOne({ id: mid })
if (products) {
res.send([products]);
} else {
res.status(404).send({message:"product not found"})
}
}));

Search mongoose array

I want to create a social network. Now I have a problem.
I have Conversations and User collection. Conversations collection schema this:
const conversationSchema = mongoose.Schema({
messages: [Object],
recipients:[{
type: Schema.Types.ObjectId,
ref: 'User'
}] ,
.....
})
I have /findOrCreate route for find recipients or create.
router.post( /findOrCreate, async ( req,res )=>{
const { selectedUsers } = req.body;
try{
const found = await Conversation.find( { recipients: selected } ); // I cant find array. Problem there
if(found) return res.status(200).json(found)
else {
// create document
}
}
} )
How to find array in mongoose. For example if I have these ['1','3','4'], ['1','2','3','4'] , ... and I my selected = ['2','4','1',3] I want to get this ['1','2','3','4'].
Please help me.

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 can i save results from mongoose query to a variable

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)
}

Categories

Resources