get length of array field in mongoose _ Nodejs - node.js

suppose we have a User model that contains an array of other User objects.
let UserSchema = mongoose.Schema({
followers: [{
type: mongoose.Schema.ObjectId,
ref: 'User',
}]
})
I need a count of this objectIds.
the first solution is to get length of followers.
req.user.followers.length
but I think it's not relevant to get all the followers that contains many of objectIds. and in my query i dont need all of this objectIds.
I tried to use virtuals but in virtuals I have many unnecessary pieces of stuff.I'm looking for the best and uncosted way for this type of situations.

because of misunderstanding your question, so I update my answer: you can use $size of mongo aggregate.
db.users.aggregate(
[
{
$project: {
id: 1,
total_followers: { $size: "$followers" }
}
}
]
)
In case of you want to find any document with specific number of length (eg: 0), you can do this :
db.users.find({ followers: { $size: 0 } } )

Related

Mongoose - How do I find a specific document by matching an element in an array of object Ids in mongoose

I have a Mongoose schema like so:
var accountSchema = new mongoose.Schema({
accountName: String,
accountType: { type: String, default: 'Individual Account' },
accountNumber: { type: String, unique: true },
accountActive: Boolean,
investment: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Investment'
}
]
})
module.exports = mongoose.model('Account', accountSchema);
And I will like to find the Account document that has a specific object id in the investment array.
"investment": [
{
"$oid": "5f353184c2daaf0f5c661ea7"
},
{
"$oid": "5f3531acc2daaf0f5c661ea9"
},
{
"$oid": "5f366e873d566938f81b94a8"
},
]
I have tried the code below but I keep getting an empty array:
Account.find().elemMatch('investment', { id: investment.id }).exec(function(err, account){
console.log(account)
});
What am I doing wrong? I expected elemMatch to work according to the mongoose document. On debug I see that the code is correctly translated to mongodb.
accounts find {"investment":{"$elemMatch":{"id":"5f3531acc2daaf0f5c661ea9"}}} {"projection":{}}
Can anyone help me out here please.
In the docs of $elemMatch, it says:
If you specify only a single condition in the $elemMatch expression, and are not using the $not or $ne operators inside of $elemMatch, $elemMatch can be omitted.
And in Query an Array for an Element:
To query if the array field contains at least one element with the specified value, use the filter { <field>: <value> } where <value> is the element value.
So in your case, you can simply do:
Account.find({investment: investment.id}).exec...
The find method retrieves entire documents. Using elemMatch as a query operator determines which document is selected from the collection. It does not change the contents of the array of sub-documents.
To limit the contents of the investment array element, use elemMatch as a projection operator.

MongoDB Searching if element exist in array

So I have something like Survey Schema (I am using mongoose).
In this Schema, for each voting option, I have votes[] array that contains ObjectIds of Users.
Now I want to check if User can vote again, or if he already voted?
The simple solution is iterating thru votes with .indexOf() and checking if id exists. Now this is a blocking way for Node JS, since this operation is sync.
Is there a way to do this with Mongo aggregate or query? So for each voting option I would get additional field like:
didIVoted: true
My Schema looks like this:
const SurveySchema = new Schema({
title: {
type: String
},
options: [{
value: String,
votes: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }]
}]
}, { timestamps: true })
You can use $addFields and $map to overwrite existing options field. To check if userId exists in votes array you can use $indexOfArray
SurveySchema.aggregate([
{
$addFields: {
options: {
$map: {
input: "$options",
in: {
value: "$$this.value",
votes: "$$this.votes",
didIVote: { $ne: [ { $indexOfArray: [ "$$this.votes", userId ] }, -1 ] }
}
}
}
}
}
])

MongoDB: use array returned from aggregation pipeline for $in query in the next stage

As the question title says, I'm trying to use an array field returned from a $match stage to query another collection in the next stage using $lookup and the $in operator to retrieve all documents that have at least one category inside this array. (I'm using Mongoose in Node, by the way)
I want to match a "configurations" collection by '_id' that have this simplified schema:
{
title: {type: String, required: true},
categories: {
allow: {type: Boolean, required: true},
list: [
{
name: {type: String, required: true},// DENORMALIZED CATEGORY NAME
_id: {type: mongoose.Schema.Types.ObjectId}
}
]
}
}
And in the next stage I want to aggregate all "partners" that belongs to at least one of those categories array. "partners" have the following schema:
{
company: {type: String, required: true},
categories: [
{type: mongoose.Schema.Types.ObjectId}
]
}
This is what I'm doing right now:
configuration.aggregate([
{$match: {_id: ObjectID(configurationId)}},
{
$lookup: {
from: "partners",
pipeline: [
{
$match: {
active: true,// MATCH ALL ACTIVE PARTNERS
categories: {
$in: {// HERE IS THE PROBLEM: I CAN'T RETRIEVE AN ARRAY FROM $map OPERATOR
$map: {// MAP CONFIGURATION CATEGORY LIST TO OUTPUT AN ARRAY ONLY WITH ID OBJECTS
input: '$categories.list',
as: 'category',
in: '$$category._id'
}
}
}
}
},
{ $project: { _id: 1, company: 1 } }
],
as: "partners"
}
},
])
The $map operator works as expected in a $project stage, but in this case I just can't use it's result as an array to be used with $in operator.
Is there any way to do this?
Thanks!
UPDATE
Doing like #Veeram suggested eliminates the need of $map operator in the $lookup stage:
{
"$lookup":{
"from":"partners",
"let":{"categories_id":"$categories.list._id"},
"pipeline":[
{"$match":{"active":true,"$expr":{"$in":["$categories","$$categories_id"]}}},
{"$project":{"_id":1,"company":1}}
],
"as":"partners"
}
}
But the problem persists with the $in operator. Like I've commented, this $in use case is the same as the 4th example in the official documentation (docs.mongodb.com/manual/reference/operator/aggregation/in), and it results in a false statement, because we are trying to check if an array ("$categories") is an element of another array ("$$categories_id"), which will fail, because the elements of "$$categories_id" are id objects and not arrays.
Does anyone know if there is any workaround for this?
Thanks!
You don't need to use $map. You can use dot notation to access the ids.
$let is required to access the values from local collection and $expr to compare the document fields.
Something like
{
"$lookup":{
"from":"partners",
"let":{"categories_id":"$categories.list._id"},
"pipeline":[
{"$match":{
"active":true,
"$expr":{
"$gt":[
{"$size":{"$setIntersection":["$categories","$$categories_id"]}},
0
]
}
}},
{"$project":{"_id":1,"company":1}}
],
"as":"partners"
}
}

Mongoose nested (2 level) find

I'm trying to use CASL for authorization check of nested items.
It uses mongoose for query data and check access.
My domain is that:
A "User" could has more "Vehicles"
A "Document" must have a Vehicle
Schema:
vehicle { users: [ {type: objectId, ref: 'user'} ] }
document { vehicle: {type: objectId, ref: 'vehicle' }}
To find the vehicle "by user" I do:
db.getCollection('vehicle').find(
{ users: {$in: [ ObjectId("5ae1a957d67500018efa2c9d") ]} }
)
That works.
In the documents collection, the data has records such as this:
{
"_id": ObjectId("5aeaad1277e8a6009842564d"),
"vehicle": ObjectId("5aea338b82d8170096b52ce9"),
"company": "Allianz",
"price": 500,
"date_start": ISODate("2018-05-02T22:00:00.000Z"),
"date_end": ISODate("2019-05-02T22:00:00.000Z"),
"createdAt": ISODate("2018-05-03T06:32:50.590Z"),
"updatedAt": ISODate("2018-05-03T06:32:50.590Z"),
"__v": 0
}
To find the document "by user" I do:
db.getCollection('document').find(
{ "vehicle.users": {$in: [ ObjectId("5ae1a957d67500018efa2c9d") ]} }
)
It doesn't work. Is possibile to do that in one single "find" query?
You can't do it in a simple MongoDB find() query, because the data about vehicle users exists in the vehicle collection, not the documents collection.
However, it is possible with an aggregation pipeline using the $lookup operator to link the data in two different collections. The aggregation would be something like this:
db.document.aggregate([
{$lookup: {
"from": "vehicle",
"localField": "vehicle",
"foreignField": "_id",
"as": "vehicleDetails",
}},
{$match: {"vehicleDetails.users" : ObjectId("5ae1a957d67500018efa2c9d")}}
])
You will probably need to add more stages to reshape the data the way you need it, but the key is to use $lookup to link the data from the two collections, then use $match to filter the set of results.
In order for this query to work you need to store users ids array in vehicle document. Neither Mongo nor CASL doesn't manage external references automatically.
Alternative solutions:
So, I see few ways:
Retrieve ids of all vehicles when you define rules. This works good in case if amount of vehicles not big (<= 1000)
const vehicleIds = await getVehicleIds(user)
can(['read', 'update'], 'document', { vehicle: { $in: vehicleIds } })
Denormalize your scheme. For example, add additional user_id field to vehicle document
Think whether you can embed document as subdocument to vechicle, something like this:
vehicle {
documents: [Document],
users: [ {type: objectId, ref: 'user'} ]
}
Just don't define rule per documents and enforce them in routes (REST or GraphQL doesn't matter).
app.get('/vehicle/:id/documents', async (req, res) => {
const vehicle = await Vehicle.findById(req.params.id)
req.ability.throwUnlessCan('read', vehicle)
const documents = Document.find({ vehicle: vehicle.id })
res.send({ documents })
})

Query/sort reference of reference in mongoose

Hopefully I can explain this well.
I have 3 Model types in play here: Users, Products, and Stores. What I'm after is a sorted list of Stores, per user, based on how many Products they've added from that Store. So basically "show me this User's top Stores".
pseudo-schemas:
var User = {
name: String
};
var Store = {
name: String
};
var Product = {
title: String,
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
}
store: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Store'
}
};
So how can I find which Stores the User has added the most Products to? This may be obvious, it's late. :-P
Thanks!
You can try to use Aggregation framework to solve it.
And especially $group pipeline:
// aggregate whole `Product` collection
Product.aggregate([
// count products by `user` and `store` and save result to `products_count`
{$group: {
_id: {user_id:"$user", store_id:"$store"},
products_count: {$sum: 1}
}},
// sort by most products count
{$sort: {products_count: -1}}
])
There are also $limit and $skip pipelines, that help to paginate all this stuff.

Resources