This question already has answers here:
How do you update objects in a document's array (nested updating)
(4 answers)
Closed 5 years ago.
This is my Schema:
const UserSchema = new mongoose.Schema({
name: String,
chats: [{
lastMessage: String,
lastUpdate: Date
}]
});
Users Collection:
{
"_id" : ObjectId("59987ef42aafc45204ee8bc3"),
"name" : "Nico",
"chats" : [
{
"_id" : ObjectId("599e58265cf2799c07925488")
"lastMessage": "Test",
"lastUpdate": "Test"
},
{
"_id" : ObjectId("599e59218d4a52c7071f46df")
"lastMessage": "Hi",
"lastUpdate": "01/01/2017"
}
]
},
{
"_id" : ObjectId("59987ef42aafc45204ee8bc3"),
"name" : "Lucas",
"chats" : [
{
"_id" : ObjectId("599e59218d4a52c7071f46df")
"lastMessage": "Hi",
"lastUpdate": "01/01/2017"
}
]
}
I am trying to implement chat on my app. I am working on my function that will find and update (on each User document, on Chat array with ObjectID as equals being received by request) but i don't know whats the best approach and how to implement it.
The request to my endpoint will give me ChatID, UserID, and MessageBody.
So my two approaches are:
1) Find by UserID, and then findAndUpdate on chat array prop where _idChat is equals as ChatID.
2) Find in Collection where _idChat is equals as ChatID and then update (this should retrieve me Users that have any object with ChatID and then update it)
This is what i am trying to implement (but having nothing being updated):
static sendMessage(req: express.Request, res: express.Response) {
const message = req.body.message;
const idChat = req.body.idChat;
const userIds = [req.body.sender, req.body.receiver];
const updatePromises = [];
userIds.forEach( userId => {
updatePromises.push(
UserDao['updateChat']
(
userId, idChat,
{'$set': {'chats.$.lastMessage': message, 'chats.$.lastUpdate': new Date() }},
);
});
Promise.all(updatePromises)
.then(values => {
return res.status(200).json(values);
}).catch(reason => {
return res.status(400).json(reason);
});
userSchema.static('updateChat', (userId, chatId, query) => {
return new Promise((resolve, reject) => {
if (!_.isObject(query)) {
return reject(new TypeError('query is not a valid Object.'));
}
if (!_.isString(userId)) {
return reject(new TypeError('userId is not a valid String.'));
}
User
.update(
{_id: userId, 'chats._id': chatId},
{$set: query},
{upsert: true}
).exec((err, updated) => {
err ? reject(err) : resolve(updated);
});
});
});
Thanks for your help!
How about this?
User.update({
_id: request.userId,
chats: {
$elemMatch: {
_id: request.chatId
}
}
}, {
$set: {
'chats.$.lastMessage': request.messageBody,
'chats.$.lastUpdate': new Date()
},
},
function(err) {
// handle error
console.log(err);
}
)
You are going to update the exact chat that matches your criteria because you'll be using the $ operator on the $set command.
Related
I need to combine results from two different documents in Mongo. I have a function like this:
async function getReviewsByUserId (req, res) {
const { userId } = req.params
const reviews = await Review.find({ userId }).lean() || []
return res.status(200).send(reviews.reverse())
}
The reviews array looks like this:
{
"_id" : ObjectId("1263b55ef2cdd3ebb0654d1dd"),
"launchId" : "7fb83b40-7c6f-4099-aaed-fe9d0dc03111",
"userId" : "1",
}
{
"_id" : ObjectId("6355565cf5ef2cddebb065584"),
"launchId" : "12b53940-136f-3399-aaed-fe9d0dc05473",
"userId" : "7fb83b40-7c6f-4099-aaed-fe9d0dc03112",
}
I need to use the launchId from each review, look up a launch object from my mongo database, and combine that with the correct object in the reviews array.
Example of what I mean:
async function getReviewsByUserId (req, res) {
const { userId } = req.params
const reviews = await Review.find({ userId }).lean() || []
const launches = await Launch.find(/* find all launches where launch._id is equal to reviews.launchId*/)
return res.status(200).send(launches.reverse())
}
So if launches data looks like this (and launches is also an array of ALL launches):
{
"_id" : "12b53940-136f-3399-aaed-fe9d0dc05473",
"name" : "The Park",
}
Then how can I merge this with the reviews payload where the launch._id == reviews.launchId so that the final data looks like this:
{
"_id" : ObjectId("1263b55ef2cdd3ebb0654d1dd"),
"launchId" : "7fb83b40-7c6f-4099-aaed-fe9d0dc03111",
"userId" : "1",
}
{
"_id" : ObjectId("6355565cf5ef2cddebb065584"),
"launchId" : "12b53940-136f-3399-aaed-fe9d0dc05473",
"userId" : "7fb83b40-7c6f-4099-aaed-fe9d0dc03112",
"launch": {
"_id" : "12b53940-136f-3399-aaed-fe9d0dc05473",
"name" : "The Park",
}
}
This could be achieved by using aggregate pipelines.
Filter reviews by userId in $match stage
$lookup for launches
$unwind the launch array to an object
The solution could be:
async function getReviewsByUserId(req, res) {
const { userId } = req.params;
const launches = await Review.aggregate([
{
$match: {
userId
}
},
{
$lookup: {
from: "launches",
localField: "launchId",
foreignField: "_id",
as: "launch"
}
},
{
$unwind: {
path: "$launch",
preserveNullAndEmptyArrays: true
}
}
]);
return res.status(200).send(launches.reverse());
}
You can map() your reviews, and for each item you can use find() on the launches array to check if the launch id matches the current review id.
If there is no match, just return the review unaltered; if you get a match, you add the launches property to the current review and return it.
function ObjectId(oid) {
return oid
}
const reviews = [{
"_id": ObjectId("1263b55ef2cdd3ebb0654d1dd"),
"launchId": "7fb83b40-7c6f-4099-aaed-fe9d0dc03111",
"userId": "1",
},
{
"_id": ObjectId("6355565cf5ef2cddebb065584"),
"launchId": "12b53940-136f-3399-aaed-fe9d0dc05473",
"userId": "7fb83b40-7c6f-4099-aaed-fe9d0dc03112",
}
]
const launches = [{
"_id": "12b53940-136f-3399-aaed-fe9d0dc05473",
"name": "The Park",
}]
const res = reviews.map(r => {
const found = launches.find(l => l._id === r.launchId)
if (found) {
r.launch = {
_id: found._id,
name: found.name
}
return r
} else {
return r
}
})
console.log(res)
Hey I was wondering how do I use findById for a schema inside an array? For example, I have the following Schema:
const GameSchema = new mongoose.Schema({
users: [
{
user: { type: mongoose.Schema.ObjectId, ref: 'User' },
role: {
type: String,
required: true,
enum: ['user', 'moderator', 'creator'],
default: 'user',
},
},
]
}]
I want to find the user with a mongoose function like findById, such as the following:
const user = await game.users.findById({ user: req.user.id })
It doesn't seem to work since users is not a mongodb model. I know I can find the user by using find() like the following:
const user = await game.users.find(
(gameUser) => gameUser.user == req.user.id
)
The only problem is that the type of gameUser and req.user.id is not the same and I can't use '==='. Is there some way to go through the array and use the mongoose function findById?
As docs explains, findById method:
Finds a single document by its _id field
So you have to use findOne() instead of findById().
Also, to return only one field from the entire array you can use projection into find.
Check this example. This query find an object by its id (i.e. user field) and return only the object, not the whole array.
db.collection.find({
"users": { "$elemMatch": { "user": 1 } }
},
{
"users.$": 1
})
Using mongoose you can do:
yourModel.findOne(({
"users": { "$elemMatch": { "user": 1 } }
},
{
"users.$": 1
})).then(result => {
console.log(result)
}).catch(e => {
// error
})
Probably a silly issue, but why is the Array.find method not working as expected when working in this case? I'm trying to query a specific comment, which involves fetching the post document that has a comments property from the DB. It is from this comments array that I'd like to extract said comment object. For whatever reason, the code below doesn't work. Why?
Below are the code snippets
// Post document from which the comments are extracted
const post = await Post.findById(postId).populate({
path: "comments",
select: "addedBy id"
});
// Resulting post.comments array
[
{ "id": "5d9b137ff542a30f2c135556", "addedBy": "5b8528131719dc141cf95c99" },
{ "id": "5d9b0ba2f28afc5c3013d4df", "addedBy": "5b8528131719dc141cf95c99" },
{ "id": "5d9b0c26f28afc5c3013d4e0", "addedBy": "5b8528131719dc141cf95c99" }
];
// For instance if commentId is '5d9b137ff542a30f2c135556'
// the resulting comment object should be {"id":"5d9b137ff542a30f2c135556","addedBy":"5b8528131719dc141cf95c99"}
// However, commentToDelete is undefined
const commentId = "5d9b137ff542a30f2c135556";
const commentToDelete = comments.find(comment => comment["id"] === commentId);
Edit: Here's the full deleteComment controller code
async function deleteComment(req, res, userId, postId, commentId) {
const post = await Post.findById(postId).populate({
path: 'comments',
select: 'addedBy id',
});
const commentToDelete = post.comments.find(
comment => comment['id'] === commentId
);
if (commentToDelete.addedBy !== userId) {
return res
.status(403)
.json({ message: 'You are not allowed to delete this comment' });
}
await Comment.findByIdAndDelete(commentId);
const updatedPost = await Post.findByIdAndUpdate(
post.id,
{ $pull: { comments: { id: commentId } } },
{ new: true, safe: true, upsert: true }
).populate(populateFields);
return res.status(200).json({ updatedPost });
}
comment => comment['id'] === commentId
Your comment subdocument comes from MongoDB/Mongoose, so comment['id'] will likely be of type ObjectID, which is never equal a string. Explicitly call the toString() function (or use some other approach for transforming to a string) before comparing:
comment => comment['id'].toString() === commentId
works fine in the below snippet, copied from your post!
I am assuming it is posts.comments in your case and not comments.find? Check for typos
const comments = [
{ "id": "5d9b137ff542a30f2c135556", "addedBy": "5b8528131719dc141cf95c99" },
{ "id": "5d9b0ba2f28afc5c3013d4df", "addedBy": "5b8528131719dc141cf95c99" },
{ "id": "5d9b0c26f28afc5c3013d4e0", "addedBy": "5b8528131719dc141cf95c99" }
];
// For instance if commentId is '5d9b137ff542a30f2c135556'
// the resulting comment object should be {"id":"5d9b137ff542a30f2c135556","addedBy":"5b8528131719dc141cf95c99"}
const commentId = "5d9b137ff542a30f2c135556";
// However, commentToDelete is undefined
const commentToDelete = comments.find(comment => comment["id"] === commentId);
console.log(commentToDelete);
you can use this :
const result = comments.find(
({ id }) => id === commentId,
);
console.log(result)
// should return { id: '5d9b137ff542a30f2c135556', addedBy: '5b8528131719dc141cf95c99' }
I'm trying to update an array in document by adding object if it doesn't exist, and replacing the object in array otherwise. But nothing ($push, $addToSet) except the $set parameter does anything, and $set works as expected - overwrites the whole array.
My mongoose schema:
var cartSchema = mongoose.Schema({
mail: String,
items: Array
});
The post request handler:
app.post('/addToCart', function(req, res) {
var request = req.body;
Cart.findOneAndUpdate({
"mail": request.mail
}, {
$addToSet: {
"items": request.item
}
}, {
upsert: true
},
function(err, result) {
console.log(result);
}
);
res.send(true);
});
The data that I'm sending from the client:
{
"mail":"test#gmail.com",
"item":{
"_id":"59da78db7e9e0433280578ec",
"manufacturer":"Schecter",
"referenceNo":"Daemon-412",
"type":"Gitare",
"image":"images/ba9727909d6c3c26412341907e7e12041507489988265.jpeg",
"__v":0,
"subcategories":[
"Elektricne"
]
}
}
EDIT:
I also get this log when I trigger 'addToCart' request:
{ MongoError: The field 'items' must be an array but is of type object in
document {_id: ObjectId('5a19ae2884d236048c8c91e2')}
The comparison in $addToSet would succeeded only if the existing document has the exact same fields and values, and the fields are in the same order. Otherwise the operator will fail.
So in your case, request.item always need to be exactly the same.
I would recommend creating a model of "item". Then, your cart schema would be like:
var cartSchema = mongoose.Schema({
mail: String,
items: [{
type: ObjectId,
ref: 'item',
}],
});
And let MongoDB determine if the item exist.
this should work you just need to implement objectExits function that test if the item is that one you're looking for :
Cart.findOne({ "mail": request.mail })
.exec()
.then(cart => {
var replaced = cart.items.some((item, i) => {
if (item._id == request.item._id)) {
cart.items[i] = request.item;
return true;
}
})
if (!replaced) {
cart.items.push(request.item);
}
cart.save();
return cart;
})
.catch(err => {
console.log(err)
});
My parent model
var GameChampSchema = new Schema({
name: String,
gameId: { type: String, unique: true },
status: Number,
countPlayers: {type: Number, default: 0},
companies: [
{
name: String,
login: String,
pass: String,
userId: ObjectId
}
],
createdAt: {type: Date, default: Date.now},
updateAt: Date
})
I need insert userId property in first child where he is not set
So, need this action only on parent with condition ({status: 0, countPlayers: { $lt: 10 })
Since this is an embedded document it is quite easy:
If you want to update a document that is the first element of the array, that doesn't have a userId
db.collection.update(
{
"status": 0,
"countPlayers": {"$lt": 10 },
"companies.userId": {"$exists": false }
},
{ "$set": {"companies.$.userId": userId } }
)
Which would be nice, but apparently this doesn't match how MongoDB processes the logic and it considers that nothing matches if there is something in the array that does have the field present. You could get that element using the aggregation framework but that doesn't help in finding the position, which we need.
A simplified proposal is where there are no elements in the array at all:
db.collection.update(
{
"status": 0,
"countPlayers": {"$lt": 10 },
"companies.0": {"$exists": false }
},
{ "$push": {"userId": userId } }
)
And that just puts a new thing on the array.
The logical thing to me is that you actually know something about this entry and you just want to set the userId field. So I would match on the login:
db.collection.update(
{
"status": 0,
"countPlayers": {"$lt": 10 },
"companies.login": login,
},
{ "$set": {"companies.$.userId": userId } }
)
As a final thing if this is just updating the first element in the array then we don't need to match the position, as we already know where it is:
db.collection.update(
{
status: 0,
countPlayers: {"$lt": 10 }
},
{ $set: { "companies.0.userId": userId } }
)
Tracing back to my logical case, see the document structure:
{
"_id" : ObjectId("530de54e1f41d9f0a260d4cd"),
"status" : 0,
"countPlayers" : 5,
"companies" : [
{ "login" : "neil" },
{ "login" : "fred", "userId" : ObjectId("530de6221f41d9f0a260d4ce") },
{ "login": "bill" },
]
}
So if what you are looking for is finding "the first document where there is no userId", then this doesn't make sense as there are several items and you already have a specific userId to update. That means you must mean one of them. How do we tell which one? It seems by the use case that you are trying to match the information that is there to an userId based on information you have.
Logic says, look for the key value that you know, and update the position that matches.
Just substituting the db.collection part for your model object for use with Mongoose.
See the documentation on $exists, as well as $set and $push for the relevant details.
Big thanks.
I solved his problem
exports.joinGame = function(req, res) {
winston.info('start method');
winston.info('header content type: %s', req.headers['content-type']);
//достаем текущего пользователя
var currentUser = service.getCurrentUser(req);
winston.info('current username %s', currentUser.username);
//формируем запрос для поиска игры
var gameQuery = {"status": 0, "close": false};
gameChamp.findOne(gameQuery, {}, {sort: {"createdAt": 1 }}, function(error, game) {
if (error) {
winston.error('error %s', error);
res.send(error);
}
//если игра нашлась
if (game) {
winston.info('Append current user to game: %s', game.name);
//добавляем userId только к одной компании
var updateFlag = false;
for (var i=0; i<game.companies.length; i++) {
if (!game.companies[i].userId && !updateFlag) {
game.companies[i].userId = currentUser._id;
updateFlag = true;
winston.info('Credentials for current user %s', game.companies[i]);
//если пользовател последний закрываем игру и отправляем в bw, что игра укомплектована
if (i == (game.companies.length-1)) {
game.close = true;
winston.info('game %s closed', game.name);
}
}
}
//сохраняем игру в MongoDB
game.save(function(error, game) {
if (error) {
winston.error('error %s', error);
res.send(error);
}
if (game) {
res.send({ game: game })
winston.info('Append successful to game %s', game.name);
}
});
}
});
}