Count the number of occurence of a nested field - node.js

I am trying to count the number of documents with a matching criteria in a nested field.
My schema is defined as follows:
let userSchema = new Schema({
//...
interests: [{
type: Schema.Types.ObjectId,
ref: 'Interests'
}],
//...
});
let interestSchema = new Schema({
id: ObjectId,
name: {
type: String,
required: true
},
//...
});
The count must reflect how many times an interest with the same name is choosed by the users.
For example, I must have a result of 2 with the interest 'coding' in the following documents:
{
//Other Fields of user 1
"interests": [
{
"id": "XXX"
"name": "coding"
},
{
"id": "YYY"
"name": "surfing"
}]
}
{
//Other Fields of user 2
"interests": [
{
"id": "ZZZ"
"name": "coding"
}
]
}
I looked into the countDocuments method, but it doesn't seem to allow this kind of count.
EDIT + First solution:
This is how I managed to solve it:
const res = await UserModel.aggregate([
{
$unwind: '$interests'
},
{
$lookup: {
from: "interests",
localField: "interests",
foreignField: "_id",
as: "interests"
}
},
{
$match:{
"interests.name": name
}
},
{
$count: "count"
}
]);
return res[0].count;
The fact that the interests is a referenced type, I can not query for its name unless I pass the lookup stage. I am not sure if this is a good solution regarding performance, since the unwind stage must pass through all the users of the database and create a new element for each of their interests. This why I am not posting it as an answer

To work with elemMatch, I had to change the schema in order to embed the Interest in the User instead of referencing it:
let userSchema = new Schema({
//...
interests: [InterestSchema],
//...
});
let InterestSchema = new Schema({
id: ObjectId,
name: {
type: String,
required: true
},
//...
});
This is how I used the elemMatch:
const count = UserModel
.where('interests').elemMatch( interest => {
interest.where({ name: name });
})
.count();
As I mentioned in my question, the aggregate method works but I am not sure about its performance since I was using a referenced array instead of a sub-document, this is why I had to change the schema of my collection

Related

How to perform filter or match in a reference field in MongoDB?

So, I have two collections named Records and Collections.
In records.model.js, I have a field which refers to Collections like this:
const Records = new mongoose.Schema(
{
collection: { type: mongoose.Schema.Types.ObjectId, ref: "Collection" },
},
);
While in the Collections, I have a field like this:
const Collection = new mongoose.Schema(
{
name: { type: String, required: true },
},
);
What I am trying to do is to perform a search in the Records with the collection name that I am getting from the req.query.
So far, I have tried:
let { text } = req.query;
const pipeline = [
{
$lookup: {
from: Collection.collection.name,
localField: "_id",
foreignField: "_id",
pipeline: [
{
$match: {
name: text
},
},
],
as: "collectionInfo",
},
}]
What I've tried is working but I want to get the results even with the substring of the name because currently, it needs the exact name for me to get the results.
If I have a query of abc, I want to get the Records with the text of abcdefg under the Collection not typing the exact words before I get the expected result. How can I do this?

mongoose $lookup always returning empty array

I have already looked up some questions related to this but with no luck at all. I have the following schemas
const Card= new mongoose.Schema(
{
name: { type: String },
collectionId: { type: mongoose.Schema.Types.ObjectId, ref: "Collection" },
price: { type: Number}
},
{ timestamps: true }
);
const Collection= new mongoose.Schema(
{
name: { type: String },
},
{ timestamps: true }
);
const Transactions = new mongoose.Schema(
{
amount: { type: Number},
cardId: { type: mongoose.Schema.Types.ObjectId, ref: "Card" },
collectionId: {type: mongoose.Schema.Types.ObjectId, ref: "Collection"},
userId: {type: mongoose.Schema.Types.ObjectId, ref: "User" }
},
{ timestamps: true }
);
lets say I want to aggregate transactions to get the user who paid most to buy cards and his info then I will do something like this
const T= require("transaction model")
const User = require("user model")
const res = await T.aggregate([
{
$group: {
_id: "$userId",
totalPaid: { $sum: "$amount" },
cardsBought: { $sum: 1 },
},
},
{
$lookup: {
from: "users" / User.document.name
localField: "userId",
foreignField: "_id",
as: "userInfo",
},
},
])
I followed similar questions answers and made sure that the collection name is correct in the "from" field (I tried both users and User.document.name) and made sure the localField and foreignField types are the same(they are both mongo ObjectId) but still I get userInfo as empty array and I am not really sure why.
A work around that I have tried is making two queries like the following
const res = await T.aggregate([])
await User.populate(res, {path: "_id", select: {...}})
but the issue is that I can't do multiple lookups (lets say I want to populate some other data as well)

Mongoose remove subdocument

I am struggling to get subdocument removed from the parent.
I am using Mongoose findOneAndUpdate.
unitRouter.delete('/:id/contracts/:cid', async (req, res) => {
Unit.findOneAndUpdate(
{ id: req.params.id },
{$pull: {contracts: { id: req.params.cid }}},
function(err, data){
console.log(err, data);
});
res.redirect(`/units/${req.params.id}`);
});
Schema is as follows:
const unitSchema = new mongoose.Schema({
address: {
type: String,
required: true
}
contracts: [{type: mongoose.Schema.Types.ObjectId, ref: 'Contract'}]
});
And it doesn't remove it from the list, neither from the contract collection.
I have checked similar topics, but didn't got it to work. What am I missing?
First of all, your schema does not match with your query.
Your schema doesn't have any id. Do you mean _id created by default?
contracts field is an array of ObjectId, not an object like { id: XXX }
So, starting from the schema you can have a collection similar to this:
[
{
"contracts": [
"5a934e000102030405000000",
"5a934e000102030405000001",
"5a934e000102030405000002"
],
"_id": "613bd938774f3b0fa8f9c1ce",
"address": "1"
},
{
"contracts": [
"5a934e000102030405000000",
"5a934e000102030405000001",
"5a934e000102030405000002"
],
"_id": "613bd938774f3b0fa8f9c1cf",
"address": "2"
}
]
With this collection (which match with your schema) you need the following query:
Unit.updateOne({
"_id": req.params.id
},
{
"$pull": {
"contracts": req.params.cid
}
})
Example here.
Also, the inverse way, your query is ok but your schema doesn't. Then you need a schema similar to this:
new mongoose.Schema(
{
id:{
type: mongoose.Schema.Types.ObjectId,
required: true
},
address: {
type: String,
required: true
},
contracts: [{
id:{
type: mongoose.Schema.Types.ObjectId,
ref: 'Contract'
}
}]
});
Example here
By the way, take care to not confuse between id and _id. By default is created the field _id.

Filter results using $match in MongoDB aggregate returning blank array

I have the following schema:
const UserQualificationSchema = new Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
qualification: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Qualification',
},
expiry_date: {
type: Date
}
}
const QualificationSchema = new Schema(
{
fleet: {
type: [String], // Eg ["FleetA", "FleetB", "FleetC"]
required: true,
}
}
I am searching the UserQualifications with filters in a table, to search them by fleet, qualification or expiry date. I so far have the following aggregate:
db.UserQualifications.aggregate([{
{
$lookup: {
from: 'qualifications',
localField: 'qualification',
foreignField: '_id',
as: 'qualification',
},
},
{
$unwind: '$qualification',
},
{
$match: {
$and: [
'qualification.fleet': {
$in: ["Fleet A", "Fleet C"], // This works
},
expiry_date: {
$lt: req.body.expiry_date, // This works
},
qualification: { // Also tried 'qualification._id'
$in: ["6033e4129070031c07fbbf29"] // Adding this returns blank array
}
]
},
}
}])
Filtering by fleet, and expiry date both work, independently and in combination, however when adding by the qualification ID, it returns blank despite the ID's being sent in being valid.
Am i missing something here?
Looking at your schema I can infer that qualification in ObjectId and in the query you are passing only the string value of ObjectId. You can pass the ObjectId to get your expected output
db.UserQualifications.aggregate([
{
$lookup: {
from: "Qualifications",
localField: "qualification",
foreignField: "_id",
as: "qualification",
},
},
{
$unwind: "$qualification",
},
{
$match: {
"qualification.fleet": {
$in: [
"FleetA",
"FleetC"
],
},
expiry_date: {
$lt: 30 // some dummy value to make it work
},
"qualification._id": {
$in: [
// some dummy value to make it work
ObjectId("5a934e000102030405000000")
]
}
},
}
])
I have created a playground with some dummy data to test the query: Mongo Playground
Also, In $match stage there is no need to combine query explicitly in $and as by default behaviour will be same as $and only so I have remove that part in my query

Mongodb update array for specific fields only

I have a json array reorderList for a topic:
const reorderList = [
{ _id: '5e6b419c76a16d5c44d87132', order: 0 },
{ _id: '5e6b41a276a16d5c44d87139', order: 1 },
{ _id: '5e6b41a776a16d5c44d87140', order: 2 }
]
And my TopicSchema is like this:
var TopicSchema = new Schema({
topicTitle: String,
topicQuestion: [
{
questionTitle: String,
answer: String,
order: Number
}
]
}
Now I want to update my topic questions order based on the reorderList's _id.
But the below statement will replace all the things from topicQuestion (e.g. questionTitle and answer will be removed)
Topic.findOneAndUpdate(
{ '_id': topicId },
{ $set: { 'topicQuestion': reorderList } }, //replaces here
{ upsert: true },
function (err, response) {
...
});
How to update it based on reorderList and also keep the original data inside topicQuestion?
The schema that you're using is badly designed. What you can do here is create another schema, TopicQuestionSchema and put a ref to the topic it belongs to.
var TopicQuestionSchema = new Schema({
questionTitle: String,
answer: String,
order: Number,
topic: {type: ObjectId, ref: 'Topic'} // the name of your model
}
This way you can still keep track of the topic the questions belong to, and still be able to update the order easily.

Resources