Mongoose update multiple subdocuments from multiple documents - mongodb

I want to update multiple subdocuments from multiple documents using mongoose.
My current code is:
const ids: any[] = payload.imageIds.map(e => new ObjectId(e));
await this.userModel
.updateMany({ 'images._id': { $in: ids } }, { $inc: { 'images.$.views': 1 } })
.exec();
And part of the schema is:
export const UserSchema = new mongoose.Schema(
{
images: [ImageSchema],
}
const ImageSchema = new mongoose.Schema(
{
views: { type: Number, default: 0 },
},
But this code only updates the last element from the ids arr.

Solved!
For those who encounter the same problem:
const imageIds: ObjectId[] = payload.imageIds.map(e => new ObjectId(e));
const userIds: ObjectId[] = payload.userIds.map(e => new ObjectId(e));
await this.userModel
.updateMany(
{ _id: { $in: userIds } },
{ $inc: { 'images.$[element].views': 1 } },
{
arrayFilters: [
{
'element._id': { $in: imageIds },
},
],
},
)
.exec();

Related

Mongoose: Filtering documents by date range returns 0 documents

I have this model:
const HistorySchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: "users",
},
category: {
type: String,
enum: category_enum,
required: true,
},
date: {
type: Date,
default: Date.now,
},
});
So a document could be:
{
"_id": "60ddad447b0e9d3c4d4fd1f1",
"user": "60dc8118118ea36a4f3cab7d",
"category": "LOGIN",
"date": "2021-03-02T00:00:00.000Z",
"__v": 0
},
I am trying to get events that happen in a given year with this:
const getEventsOfYear = async (year) => {
let response = {
events_of_year_per_user: null,
};
const start_date_of_the_year = moment(year);
const end_date_of_the_year = moment(year).endOf("year");
const filter_stage = {
$match: {
date: {
$gte: start_date_of_the_year,
$lte: end_date_of_the_year,
},
},
};
const pipeline = [filter_stage];
const history_events_with_aggregate = await History.aggregate(pipeline);
response.events_of_year_per_user = history_events_with_aggregate;
return response;
};
The problem is that this always returns an empty array:
{
"events_of_year_per_user": []
}
Any idea what I'm doing wrong?
EDIT 1:
I even tried with another model and direct date input instead of using moment and it's still the same result:
const filter_stage = {
$match: {
date: {
$gte: "2022-01-01",
$lte: "2022-12-30",
},
},
};
const pipeline = [filter_stage];
const history_events_with_aggregate = await userModel.aggregate(pipeline);
But, using find works:
const history_events_with_aggregate = await userModel.find({
date: { $gte: "2022-01-01", $lte: "2022-12-30" },
});
This is how I solved the issue:
const filter_stage = {
$match: {
date: {
$gte: new Date("2022-01-01"),
$lte: new Date("2022-12-30"),
},
},
};
And if you want to use moment:
const start_date_of_the_year = moment(year);
const end_date_of_the_year = moment(year).endOf("year");
const filter_stage = {
$match: {
date: {
$gte: new Date(start_date_of_the_year),
$lte: new Date(end_date_of_the_year),
},
},
};
You can also do this:
const today = moment().startOf("day");
const filter_stage = {
$match: {
user: ObjectId(user_id),
date: {
$gte: today.toDate(),
$lte: moment(today).endOf("day").toDate(),
},
},
};

Upsert in nested array doesn't create parent document

Schema
{
chapter: {
required: true,
type: Schema.Types.ObjectId,
ref: "Chapter",
},
questions: {
type: [Number]
},
};
Here is an example document
{
"_id":{
"$oid":"5ff4b728b6af610f0851d2a6"
},
"chapters":[
{
"chapter":{
"$oid":"611478ab34dde61f28dbe4d3"
},
"questions":[
35,
29,
167,
180,
101,
16,
71,
23
]
},
{
"chapter":{
"$oid":"611478ac34dde61f28dbe4d8"
},
"questions":[
162
]
}
]
}
I want to "$addToSet" on "questions", such as
const someId = SOME_ID;
const chapterId = "611478ac34dde61f28dbe4d8";
const update = {
$addToSet: {
"chapters.$.questions": {
$each: [5, 10, 32, 6],
},
},
};
await model.findOneAndUpdate(
{
_id: someId,
"chapters.chapter": chapterId,
},
update,
{ upsert: true }
)
.lean()
.exec();
This works. However, if there is no document, the "upsert" doesn't create the document.
How can I rewrite the operation so that it can update (addToSet) as well as ensure the document is created if it didn't exist?
I checked MongoDB native query use these
db.con.collection('example').updateOne(
{"chapters": {$elemMatch:{"chapter.id":ObjectId("611478ac34dde61f28dbe4d8")}}},
{$addToSet: {
"chapters.$.questions": {
$each: [5, 10, 32, 6],
},
}},
{upsert: true})
you should find the element of array using elemMatch
{"chapters": {$elemMatch:{"chapter.id":"611478ac34dde61f28dbe4d8"}}}
I figured out, for some reason, I can't $addToSet if the parent object is not present. So I had to make one more operation.
Inspired from this Stackoverflow answer.
I fetch the "chapters" which I need to add.
From this list of fetched chapters, I check which ones exist and which ones don't.
Using the knowledge from point 2, I am using $push to add the chapters which didn't exist entirely, and "adding to set ($addToSet)" questions on the chapters which do exist.
I am posting the code which works for me.
//Data to add (which chapter?: questionNumber[])
const docId = "SOMEID";
const questionsToAdd = {
"611478ab34dde61f28dbe4d3": [1,5,6,10],
"611478ab34dde61f28dbe4d8": [5,8,20,30],
};
//Find the chapters from questionsToAdd which exist
const existingDoc = await model.findOne({
_id: docId,
chapters: { $elemMatch: { chapter: { $in: Object.keys(questionsToAdd) } } },
})
.select(["chapters.chapter"])
.lean()
.exec();
// Objectify the array of chapters
const existingChapters = (existingDoc?.chapters ?? []).map((x) => "" + x.chapter);
// Prepare what to insert, what to update
const updateObject = {
$addToSet: {},
arrayFilters: [],
$push: [],
};
for (const [index, [chapterId, questionIndices]] of Object.entries(questionsToAdd).entries()) {
if (existingChapters.includes(chapterId)) {
updateObject.$addToSet["chapters.$[filter" + index + "].questions"] = { $each: questionIndices };
updateObject.arrayFilters.push({
["filter" + index + ".chapter"]: Types.ObjectId(chapterId),
});
} else {
updateObject.$push.push({
chapter: chapterId,
questions: questionIndices,
});
}
}
if (updateObject.arrayFilters.length) {
// *Add to set the chapters which exist
await model.findOneAndUpdate(
{ _id: userId },
{
$addToSet: updateObject.$addToSet,
},
{
arrayFilters: updateObject.arrayFilters,
upsert: true,
}
)
.lean()
.exec();
}
if (updateObject.$push.length) {
// *Push the ones that does not exist
await model.findOneAndUpdate(
{ _id: userId },
{
$push: { chapters: updateObject.$push },
},
{
upsert: true,
}
)
.lean()
.exec();
}

How do I count all the documents in a collection and use the cont in a controller, with MongoDB and Express.js?

I am working on a blogging application (click the link to see the GitHub repo) with Express (version 4.17.1), EJS and MongoDB (version 4.0.10).
Trying to paginate the posts I did the following, in the controller:
exports.getPosts = (req, res, next) => {
const perPage = 5;
const currPage = req.query.page ? parseInt(req.query.page) : 1;
let postsCount = 0;
const posts = Post.find({}, (err, posts) => {
postsCount = posts.length;
let pageDecrement = currPage > 1 ? 1 : 0;
let pageIncrement = postsCount >= perPage ? 1 : 0;
if (err) {
console.log('Error: ', err);
} else {
res.render('default/index', {
moment: moment,
layout: 'default/layout',
website_name: 'MEAN Blog',
page_heading: 'XPress News',
page_subheading: 'A MEAN Stack Blogging Application',
currPage: currPage,
posts: posts,
pageDecrement: pageDecrement,
pageIncrement: pageIncrement
});
}
})
.sort({
created_at: -1
})
.populate('category')
.limit(perPage)
.skip((currPage - 1) * perPage);
};
And in the view:
<a class="btn btn-primary <%= pageDecrement == 0 ? 'disabled' : '' %>" href="/?page=<%= currPage - pageDecrement %>">← Newer Posts</a>
and
<a class="btn btn-primary <%= pageIncrement == 0 ? 'disabled' : '' %>" href="/?page=<%= currPage + pageIncrement %>">Older Posts →</a>
That works fine unless there are is a number of posts equal to perPage x N, where N is an integer, in which case the "Older Posts" button becomes disabled one page too late.
That is because postsCount = posts.length counts the posts after they are limited by .skip((currPage - 1) * perPage).
So I need to count the posts from the model/collection and bring that count variable in the controller.
My model:
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
short_description: {
type: String,
required: true
},
full_text: {
type: String,
required: true
},
category: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Category'
},
post_image: {
type: String,
required: false
},
updated_at: {
type: Date,
default: Date.now()
},
created_at: {
type: Date,
default: Date.now()
}
});
module.exports = mongoose.model('Post', postSchema);
How do I count all the documents in the posts collection and use that number in the posts controller?
This can be done easier with mongodb aggregation framework.
We use $facet aggregation to get the paginated data along with the total number of documents.
In aggregation framework we use $lookup instead of mongoose populate. $lookup returns an array, to get the first item in array we use $arrayElemAt operator inside $addFields.
Playground
And here is the code to apply to your app:
(The first $match aggregation is unnecessary here, but I put in in case you may need it in the future)
exports.getPosts = async (req, res, next) => {
const perPage = 5;
const currPage = req.query.page ? parseInt(req.query.page) : 1;
const skip = (currPage - 1) * perPage;
try {
const result = await Post.aggregate([{
$match: {},
},
{
$sort: {
created_at: -1,
},
},
{
$lookup: {
from: "categories",
localField: "category",
foreignField: "_id",
as: "category",
},
},
{
$addFields: {
category: {
$arrayElemAt: ["$category", 0],
},
},
},
{
$facet: {
totalRecords: [{
$count: "total",
}, ],
data: [{
$skip: skip,
},
{
$limit: perPage,
},
],
},
},
]);
let postsCount = result[0].totalRecords[0].total;
const pageCount = Math.ceil(postsCount / perPage);
const pageDecrement = currPage > 1 ? 1 : 0;
const pageIncrement = currPage < pageCount ? 1 : 0;
const posts = result[0].data;
res.render("default/index", {
moment: moment,
layout: "default/layout",
website_name: "MEAN Blog",
page_heading: "XPress News",
page_subheading: "A MEAN Stack Blogging Application",
currPage,
posts,
pageDecrement,
pageIncrement,
});
} catch (err) {
console.log("Error: ", err);
res.status(500).send("something went wrong");
}
};
By the way, in the post schema, for date fields you use default: Date.now(), this will cause the date value always the same value, it should be in this format: default: Date.now
Read $facet.
New in version 3.4.
Processes multiple aggregation pipelines within a single stage on the
same set of input documents. Each sub-pipeline has its own field in
the output document where its results are stored as an array of
documents.
Example: See here
db.collection.aggregate([
{
$facet: {
"count": [
{ $match: {} },
{ $count: "totalCount" }
],
"data": [
{ $match: {} },
{ $sort: { _id: -1 } },
{ $skip: 1 },
{ $limit: 2 }
]
}
}
])
Mongoose Version:
Model.aggregate([
{
$facet: {
"count": [
{ $match: {} },
{ $count: "totalCount" }
],
"data": [
{ $match: {} },
{ $sort: { _id: -1 } },
{ $skip: 1 },
{ $limit: 2 }
]
}
}
]).
then(res => console.log(res)).
catch(error => console.error('error', error));
In case of Mongoose you should use this:
https://mongoosejs.com/docs/api.html#aggregate_Aggregate-facet
Official Mongodb docs:
https://docs.mongodb.com/manual/reference/operator/aggregation/facet
General idea is to perform aggregation instead of multiple calls (1 for getting needed info + 1 to get the total count of documents)
You can perform 2 separate calls of course but it will hit your performance (not much for small data volumes but still...)
So you can get all needed data with .find() and then get count like this:
https://mongoosejs.com/docs/api.html#model_Model.count
PS. btw, use async/await instead of callbacks to avoid callback hell

Can Update document with mongodb query but not work when do in mongoose [duplicate]

This question already has answers here:
Update nested subdocuments in MongoDB with arrayFilters
(2 answers)
Closed 3 years ago.
My collection is like this: https://mongoplayground.net/p/91InBXrUq7R
With this query I can update replies.likes
db.getCollection("posts").updateOne(
{
"_id": ObjectId("5da832caeb173112348e509b"), //posts._id
"comments.replies._id":ObjectId("5db6a88f7c6cfb0d0c2b689b"),//replies._id
},
{ "$push": { "comments.$[outer].replies.$[inner].likes": "10000012" } },
{
"arrayFilters": [
{ "outer._id": ObjectId("5db06e11d0987d0aa2cd5593") },//comments._id
{ "inner._id": ObjectId("5db6a88f7c6cfb0d0c2b689b") }//replies._id
]
}
)
But when I code using mongoose, express, collection not update
//Like Reply toggle
router.post("/toggleLikeReply", function(req, res, next) {
var id_post = req.body.id_post;
var id_comment = req.body.id_comment;
var id_reply = req.body.id_reply;
var id_user = req.user._id;
console.log("id_post: "+id_post+" id_comment: "+id_comment+" id_reply: "+id_reply+" id_user: "+id_user);
//todo
Post.aggregate([
{ $match: {_id: ObjectId(id_post),"comments._id": ObjectId(id_comment)}},
{ $unwind: "$comments"},
{ $match: { "comments._id": ObjectId(id_comment)}},
{ $project: {"replies": "$comments.replies", _id: 0}},
{ $match: { "replies._id": ObjectId(id_reply)}},
{ $project: {"likes": "$replies.likes", _id: 0}},
]).exec((err, users_liked) => {
var index = users_liked[0].likes[0].indexOf(id_user);
console.log(users_liked[0].likes[0]);
//todo
if (index == -1) {
const updatePost = async () => {
try {
await Post.updateOne({
_id: ObjectId(req.body.id_post),
"comments.replies._id": ObjectId(req.body.id_reply)},
{ $push: {"comments.$[outer].replies.$[inner].likes": ObjectId(req.user._id)} },
{
"arrayFilters": [
{ "outer._id": ObjectId(req.body.id_comment) },
{ "inner._id": ObjectId(req.body.id_reply) }
]
}
);
} catch (error) {
console.log("error", error);
}
};
updatePost().then(function(data) {res.send({ like: true, success: true})});
}else{
const updatePost = async () => {
try {
await Post.updateOne({
_id: ObjectId(req.body.id_post),
"comments.replies._id": ObjectId(req.body.id_reply)},
{ $pull: {"comments.$[outer].replies.$[inner].likes": ObjectId(req.user._id)} },
{
"arrayFilters": [
{ "outer._id": ObjectId(req.body.id_comment) },
{ "inner._id": ObjectId(req.body.id_reply) }
]
}
);
} catch (error) {
console.log("💥", error);
}
};
updatePost().then(function(data) {res.send({ like: false, success: true})});
}
})
});
I logged the all the id is come and the same as I did with mongo query directly .
id_post: 5da832caeb173112348e509b
id_comment: 5db06e11d0987d0aa2cd5593
id_reply: 5db6a88f7c6cfb0d0c2b689b
id_user: 5da85558886aee13e4e7f044
What is wrong with my code using mongoose and express?
Try This Query
var mongoose = require('mongoose');
const Schema = mongoose.Schema
const ObjectId = Schema.Types.ObjectId
const updatePost = async () => {
try {
await Post.updateOne({
_id: ObjectId(req.body.id_post),
"comments.replies._id": ObjectId(req.body.id_reply)},
{ $push: {"comments.$[outer].replies.$[inner].likes": req.user._id} },
{
"arrayFilters": [
{ "outer._id": ObjectId(req.body.id_comment) },
{ "inner._id": ObjectId(req.body.id_reply) }
]
}
);
} catch (error) {
console.log("error", error);
}
};
updatePost().then(function(data) {res.send({ like: true, success: true})});

Mongoose: Filter doc and manipulate nested array

I have an image schema that has a reference to a category schema and a nested array that contains an object with two fields (user, createdAt)
I am trying to query the schema by a category and add two custom fields to each image in my query.
Here is the solution with virtual fields:
totalLikes: Count of all nested attributes
schema.virtual("totalLikes").get(function() {
return this.likes.length;
});
canLike: Check if user with id "5c8f9e676ed4356b1de3eaa1" is included in the nested array. If user is included it should return false otherwise true
schema.virtual("canLike").get(function() {
return !this.likes.find(like => {
return like.user === "5c8f9e676ed4356b1de3eaa1";
});
});
In sql it would be a simple SUBQUERY but I can't get it working in Mongoose.
Schema:
import mongoose from "mongoose";
const model = new mongoose.Schema(
{
category: {
type: mongoose.Schema.Types.ObjectId,
ref: "Category"
},
likes: [{
user: {
type: String,
required: true
},
createdAt: {
type: Date,
required: true
}
}]
})
here is a sample document:
[{
category:5c90a0777952597cda9e9c8d,
likes: [
{
_id: "5c90a4c79906507dac54e764",
user: "5c8f9e676ed4356b1de3eaa1",
createdAt:"2019-03-19T08:13:59.250+00:00"
}
]
},
{
category:5c90a0777952597cda9e9c8d,
likes: [
{
_id: "5c90a4c79906507dac54e764",
user: "5c8f9e676ed4356b1dw223332",
createdAt:"2019-03-19T08:13:59.250+00:00"
},
{
_id: "5c90a4c79906507dac54e764",
user: "5c8f9e676ed4356b1d8498933",
createdAt:"2019-03-19T08:13:59.250+00:00"
}
]
}]
Here is how it should look like:
[{
category:5c90a0777952597cda9e9c8d,
likes: [
{
_id: "5c90a4c79906507dac54e764",
user: "5c8f9e676ed4356b1de3eaa1",
createdAt:"2019-03-19T08:13:59.250+00:00"
}
],
totalLikes: 1,
canLike: false
},
{
category:5c90a0777952597cda9e9c8d,
likes: [
{
_id: "5c90a4c79906507dac54e764",
user: "5c8f9e676ed4356b1dw223332",
createdAt:"2019-03-19T08:13:59.250+00:00"
},
{
_id: "5c90a4c79906507dac54e764",
user: "5c8f9e676ed4356b1d8498933",
createdAt:"2019-03-19T08:13:59.250+00:00"
}
],
totalLikes: 2,
canLike: true
}]
Here is what I tried:
Resolver:
1) Tried in Mongoose call - Fails
const resources = await model.aggregate([
{ $match: {category: "5c90a0777952597cda9e9c8d"},
$addFields: {
totalLikes: {
$size: {
$filter: {
input: "$likes",
as: "el",
cond: "$$el.user"
}
}
}
},
$addFields: {
canLike: {
$match: {
'likes.user':"5c8f9e676ed4356b1de3eaa1"
}
}
}
}
])
2) Tried to change it after db call - works but not preferred solution
model.where({ competition: "5c90a0777952597cda9e9c8d" }).exec(function (err, records) {
resources = records.map(resource => {
resource.likes = resource.likes ? resource.likes: []
const included = resource.likes.find(like => {
return like.user === "5c8f9e676ed4356b1de3eaa1";
});
resource.set('totalLikes', resource.likes.length, {strict: false});
resource.set('canLike', !included, {strict: false});
return resource
});
})
Does anyone know how I can do it at runtime? THX
you can achieve it using aggregate
Model.aggregate()
.addFields({ // map likes so that it can result to array of ids
likesMap: {
$map: {
input: "$likes",
as: "like",
in: "$$like.user"
}
}
})
.addFields({ // check if the id is present in likesMap
canLike: {
$cond: [
{
$in: ["5c8f9e676ed4356b1de3eaa1", "$likesMap"]
},
true,
false
]
},
totalLikes: {
$size: "$likes"
}
})
.project({ // remove likesMap
likesMap: 0,
})