Using $match on an embedded document in an aggregate - javascript

I am trying to use $match to find items with a specific _id in a double embedded document.
I have a document called users which contains information such as name, email, and it also contains an embedded document which has the business this user is with.
I also have a document called businesses, which contains an embedded document which has the building that this business is in.
I also have a document called building.
I am trying to have a mongo query which returns all of the users with a business at a certain building ID.
I have an aggregate function which uses $lookup to match the users to the building they are in. and this does work. However now I am trying to use $match to only return the documents with a specific building id.
Here is an example of my user, business and building documents:
_id: 5ca487c0eeedbe8ab59d7a7a
name: "John Smith"
email: "jsmith9#gmail.com"
business: Object
_id: 5ca48481eeedbe8ab59d7a38
name: "Visitors"
_id: 5ca48481eeedbe8ab59d7a38
name: "Visitors"
building: Object
_id: 5ca48481eeedbe8ab59d7a36
name: "Building1"
_id: 5ca48481eeedbe8ab59d7a36
name: "Building1"
When I return the aggregated query it returns documents in the following format:
{
"_id": "5ca487c0eeedbe8ab59d7a7a",
"name": "John Smith",
"email": "jsmith9#gmail.com",
"business": {
"_id": "5ca48481eeedbe8ab59d7a38",
"name": "Visitors"
},
"__v": 0,
"user_building": {
"_id": "5ca48481eeedbe8ab59d7a38",
"name": "Visitors",
"building": {
"_id": "5ca48481eeedbe8ab59d7a36",
"name": "Building1"
},
"__v": 0
}
},
However when I add the match in, it returns []. What am i doing wrong here?
router.get("/:id", async (req, res) => {
const users_buildings = await User.aggregate([
{
$lookup: {
from: "businesses",
localField: "business._id",
foreignField: "_id",
as: "user_building"
}
},
{ $unwind: "$user_building" },
{
$match: {
"user_building.building": { _id: req.params.id }
}
}
]);

You need to match _id inside the building object. Try with this
{
$match: {
"user_building.building._id": req.params.id
}
}
if not working
{
$match: {
"user_building.building._id": ObjectId(req.params.id)
}
}
op edit: I imported ObjectId with:
var ObjectId = require('mongodb').ObjectID;
and used the second solution and it worked correctly.

Related

Mongoose : find document with subdocument filter

I have an User like so the id is NOT the _id)
{
id: string;
}
Which can create files like so
{
name: string;
author: User;
}
I would like to get all Files where the author is a given User, but I do not know how to use the "filter" function to do that.
So currently I do
const author = await this.userModel.find({ id });
return this.filesModel.find({ author });
Is there a more efficient way to do it ?
(I use NestJS with the Mongoose integration, the syntax used is the same as the Mongoose library)
EDIT
Given the User document
{
_id: 'OVZVIbovibiugb44'
id: 10
}
And the Files documents
[
{ name: "1.docx", author: ObjectId('OVZVIbovibiugb44') },
{ name: "2.docx", author: ObjectId('voisbvOVISBEIVBv') },
]
I would like to use the function
findOwned(authorId = 10) {
const author = await this.userModel.find({ id });
return this.filesModel.find({ author });
// But do it only with "filesModel"
}
And get, as a result,
[
{ name: '1.docx', author: 'ObjectId('OVZVIbovibiugb44') },
]
You can use $lookup into an aggregation query to merge collections.
Also, as your id is an String and your author is an ObjectId you will need one previous stage using $toObjectId
So the query is similar to this:
$match stage (optional) to query only with documents you want. Like a filter
$project to convert id String field to ObjectId. You can user $set also.
$lookup to merge collection and the ouput is in a field called files.
$project to output only files array from the merge.
db.User.aggregate([
{ "$match": { "id": "5a934e000102030405000001" } },
{ "$project": { "id": { "$toObjectId": "$id" } } },
{ "$lookup": {
"from": "Files",
"localField": "id",
"foreignField": "author",
"as": "files" }
},
{ "$project": { "files": 1 } }
])
Example here

Mongoose find documents by child criteria

I'm struggling trying to find all the docs with a specific property in the child. For example, I want to find all the users with their child active.
These are my models
const userSchema = new mongoose.Schema({
child: {
type: mongoose.Schema.Types.ObjectId,
ref: 'child'
}
});
const childSchema = new mongoose.Schema({
active: {
type: Boolean,
default: true
}
});
I tried with populate and match ( .populate({path:'child', match: {active: true}})) but I'm getting all the users with the property child as null if not active. I need only the users with an active child. All my researches head to use the dot syntax, but for any reason I get an empty array. See below:
let usersWithActiveChild = await User.find({'child.active': true}));
console.log(usersWithActiveChild) // --> displays '[]'
Thanks for your help!
This can be accomplished easily by using aggregation framework.
First we join two collections with $lookup.
Lookup result is array, but our relation with User and Child is one to one, so we get the first item by using $arrayElemAt: ["$child", 0].
And lastly, we apply our filter "child.active": true, using $match.
Playground
let usersWithActiveChild = await User.aggregate([
{
$lookup: {
from: "childs", //must be PHYSICAL collection name
localField: "child",
foreignField: "_id",
as: "child",
},
},
{
$addFields: {
child: {
$arrayElemAt: ["$child", 0],
},
},
},
{
$match: {
"child.active": true,
},
},
]);
Sample docs:
db={
"users": [
{
"_id": ObjectId("5a834e000102030405000000"),
"child": ObjectId("5a934e000102030405000000")
},
{
"_id": ObjectId("5a834e000102030405000001"),
"child": ObjectId("5a934e000102030405000001")
},
{
"_id": ObjectId("5a834e000102030405000002"),
"child": ObjectId("5a934e000102030405000002")
},
],
"childs": [
{
"_id": ObjectId("5a934e000102030405000000"),
"active": true
},
{
"_id": ObjectId("5a934e000102030405000001"),
"active": false
},
{
"_id": ObjectId("5a934e000102030405000002"),
"active": true
}
]
}
Output:
[
{
"_id": ObjectId("5a834e000102030405000000"),
"child": {
"_id": ObjectId("5a934e000102030405000000"),
"active": true
}
},
{
"_id": ObjectId("5a834e000102030405000002"),
"child": {
"_id": ObjectId("5a934e000102030405000002"),
"active": true
}
}
]
Or as a better approach would be first getting activ childs, and then lookup with users like this:
db.childs.aggregate([
{
$match: {
"active": true
}
},
{
$lookup: {
from: "users",
localField: "_id",
foreignField: "child",
as: "user"
}
}
])
Playground
When you use a ref to refer to another schema, Mongoose stores the documents in separate collections in MongoDB.
The actual value stored in the child field of the user document is a DBRef.
If you were to look at the data directly in MongoDB you would find something similar to this:
User collection
{
_id: ObjectId("5a934e000102030405000000")
child: DBRef("child",ObjectId("5a934e000102030405000001"),"DatabaseName")
}
Child collection:
{
_id: ObjectId("5a934e000102030405000001"),
active: true
}
When you populate the user object, Mongoose fetches the user document, and then fetches the child. Since the user documents have been retrieved already, the match in the populate call filters the children, as you noted.
The dotted notation 'child.active' can only be used if the child is stored in MongoDB as a subdocument, like
{
_id: ObjectId("5a934e000102030405000000")
child:{
_id: ObjectId("5a934e000102030405000001"),
active: true
}
}
But your child is defined as a ref, so this will not be the case.
In order to filter the list of user documents based on the content of the referenced child, you will need to either
- populate with match as you have done and then filter the result set, or
- aggregate the user collection, lookup the child documents, and then match the child field.

MongoDb-Node:Agregation node-mongo query for chat

im new on mongo and want to get data for a chat, let me explain.
i have a colection of messages:
_id:id
viewed:boolean
created_at:date
text:String
receiver:ObjectId
emitter:ObjectId
i want all the list of messager for a certain emitter and receiver order by the date (like a normal chat)
i have tryed aggregation like this:
db.messages.aggregate(
[
{
$lookup: {
from: "users",
localField: "emitter", // field in the orders collection
foreignField: "_id", // field in the items collection
as: "fromItems"
}
},
{
$match: {
'emitter':ObjectId("5c8917b4ef9ebf2e608c68dc")
}
}
,
{
$addFields: {
ids: { _id: "$_id" } ,
created: { created_at: "$created_at" }
}
},
{
$group:
{
_id: { tot:["$emitter", "$receiver"] },
text: { $addToSet:"$text"},
}
},
{
$sort: {created_at: 1}
}
]
)
But this gives me an array of messages only of a certain emitter and dont give me the date or the viewed data.
Im really new on mongo and node so if someone can help me with a explain will be great.
Thanks for reading and sory for the bad english
You must include date or the viewed data in $group stage.
Try with this.
{
$group:
{
_id: { tot:["$emitter", "$receiver"] },
text: { $addToSet:{text:"$text",created:"$created.created_at"}},
created_at:{$last:"$created.created_at"}
}
},
Why there is ids and need for tot fields and created as a object ??

Sorting and grouping nested subdocument in Mongoose

I have a schema, Comment, like the one below. It's a system of "comments" and "replies", but each comment and reply has multiple versions. When a user wants to view a comment, I want to return just the most recent version with the status of APPROVED.
const Version = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
body: String,
created: Date,
title: String,
status: {
type: String,
enum: [ 'APPROVED', 'OPEN', 'CLOSED' ]
}
})
const Reply = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
created: Date,
versions: [ Version ]
})
const Comment = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
created: Date,
versions: [ Version ],
replies: [ Reply ]
})
I've gotten the parent Comment to display how I want with the code below. However, I've had trouble applying that to the sub-document, Reply.
const requestedComment = yield Comment.aggregate([
{ $match: {
query
} },
{ $project: {
user: 1,
replies: 1,
versions: {
$filter: {
input: '$versions',
as: 'version',
cond: { $eq: [ '$$version.status', 'APPROVED' ] }
}
},
}},
{ "$unwind": "$versions" },
{ $sort: { 'versions.created': -1 } },
{ $group: {
_id: '$_id',
body: { $first: '$versions.body' },
title: { $first: '$versions.title' },
replies: { $first: '$replies' }
}}
])
.exec()
Any help achieving the same result with the replies subdocuments would be appreciated. I would like to return the most recent APPROVED version of each reply in a form like this:
comment: {
body: "The comment's body.",
user: ObjectId(...),
replies: [
{
body: "The reply's body."
user: ObjectId(...)
}
]
}
Basically you just need to continue the same process on from the existing pipeline. But this time to $unwind out the "versions" per each "replies" entry and $sort them there.
So these are "additional" stages to your pipeline.
// Unwind replies
{ "$unwind": "$replies" },
// Unwind inner versions
{ "$unwind": "$replies.versions" },
// Filter for only approved
{ "$match": { "replies.versions.status": "APPROVED" } },
// Sort on all "keys" and then the "version" date
{ "$sort": {
"_id": 1,
"replies._id": 1,
"replies.versions.created": -1
}},
// Group replies to get the latest version of each
{ "$group": {
"_id": {
"_id": "$_id",
"body": "$body",
"title": "$title",
"replyId": "$replies._id",
"replyUser": "$replies.user",
"replyCreated": "$replies.created"
},
"version": { "$first": "$replies.version" }
}},
// Push replies back into an array in the main document
{ "$group": {
"_id": "$_id._id",
"body": { "$first": "$_id.body" },
"title": { "$first": "$_id.title" },
"replies": {
"$push": {
"_id": "$_id.replyId",
"user": "$_id.replyUser" },
"created": "$_id.replyCreated", // <-- Value from Reply
"body": "$version.body", // <-- Value from specific Version
"title": "$version.title"
}
}
}}
All depending of course on which fields you want, being either from ther Reply or from the Version.
Whichever fields, since you "un-wound" two arrays, you $group back "twice".
Once to get the $first items after sorting per Reply
Once more to re-construct the "replies" array using $push
That's all there is too it.
If you were still looking at ways to "sort" the array "in-place" without using $unwind, well MongoDB just does not do that yet.
Bit of advice on your design
As a note, I see where you are going with this and this is the wrong model for the type of usage that you want.
It makes little sense to store "revision history" within the embdedded structure. You are rarely going to use it in general update and query operations, and as this demonstrates, most of the time you just want the "latest".
So just do that instead, and store a "flag" indicating "revisions" if really necessary. That data can then be stored external to the main structure, and you won't have to jump through these hoops just to get the "latest accepted version" on every request.

How to group records by a field in Mongoose?

I've got a MongoDB document which has records that looks like this:
[
{ _id: id, category_name: 'category1', parent_category: null },
{ _id: id, category_name: 'category2', parent_category: null },
{ _id: id, category_name: 'subcategory1', parent_category: id_parent_1 },
{ _id: id, category_name: 'subcategory2', parent_category: id_parent_1 },
{ _id: id, category_name: 'subcategory3', parent_category: id_parent_2 },
{ _id: id, category_name: 'subcategory4', parent_category: id_parent_2 }
]
As you can see, I'm storing categories with a parent_category of null, and subcategories have the parent category's ID. What I'm looking for is to group these into some kind of format like this:
[
{ category_name: 'category1', categories: [
{ category_name: 'subcategory1', _id: id },
{ category_name: 'subcategory2', _id: id }
]
},
{ category_name: 'category2', categories: [
{ category_name: 'subcategory3', _id: id },
{ category_name: 'subcategory4', _id: id }
]
}
]
So basically group the parent categories with an array with their child categories. I'm using Mongoose. I tried using the aggregation framework MongoDB provides but I can't get the desired result. :(
I have access to modify the schema in any way that could be needed!
Thanks in advance!
It seems like you're treating Mongo like an relational database (separating all these fields and bringing them together with a query). What you should do is rebuild your Schema. For example:
var CategorySchema = new Schema({
category_name: String,
subCategories:[subCategorySchema]
}
var subCategorySchema = new Schema({
category_name: String
})
This way when you need to query the collection it's a simple
db.find({category_name: "name of the category"}, function(){})
to get everything you need.
Just in case: you can add the sub categories to the array with simple updates. Read this for more info.
Please try this if Your schema is not changed:
var MongoClient = require('mongodb').MongoClient
//connect away
MongoClient.connect('mongodb://127.0.0.1:27017/test', function(err, db) {
if (err) throw err;
console.log("Connected to Database");
//simple json record
var document = [];
//insert record
//db.data.find({"parent_category":null }).forEach(function(data) {print("user: " + db.data.findOne({"parent_category":data._id })) })
db.collection('data').find({"parent_category":null }, function(err, parentrecords) {
if (err) throw err;
var cat ={};
parentrecords.forEach(function(data){
cat["category_name"] = data["category_name"];
db.collection('data').find({"parent_category":data._id },function(err, childrecords) {
var doc = [];
childrecords.forEach(function(childdata){
doc.push(childdata);
},function(){
cat["categories"] = doc;
document.push(cat);
console.log(document);
});
});
});
});
});
If you want to find out expected results without changing schema then you basically follow some complex mongo aggregation query. For finding output I follow following steps :
First in $project check parent_category equals null if true then add _id else add parent_category.
Now document structure looks like with new key name as parent_id presents and group by parent_id and push remaining data like category_name and parent_category.
After that use $setDifference and $setIntersection to differentiate parent data and child data.
And in finally unwind only single array objects so this single array object and used project for showing only those fields which to display.
Check working aggregation query as below :
db.collectionName.aggregate({
"$project": {
"parent_id": {
"$cond": {
"if": {
"$eq": ["$parent_category", null]
},
"then": "$_id",
"else": "$parent_category"
}
},
"category_name": 1,
"parent_category": 1
}
}, {
"$group": {
"_id": "$parent_id",
"categories": {
"$push": {
"category_name": "$category_name",
"parent_category": "$parent_category"
}
},
"parentData": {
"$push": {
"$cond": {
"if": {
"$eq": ["$parent_category", null]
},
"then": {
"category_name": "$category_name",
"parent_category": "$parent_category"
},
"else": null
}
}
}
}
}, {
"$project": {
"findParent": {
"$setIntersection": ["$categories", "$parentData"]
},
"categories": {
"$setDifference": ["$categories", "$parentData"]
}
}
}, {
"$unwind": "$findParent"
}, {
"$project": {
"category_name": "$findParent.category_name",
"categories": 1
}
}).pretty()
In order to group records by a field try using $group in Aggreegation.This worked for me.
Example
db.categories.aggregate(
[
{ $group : { _id : {category_name:"$category_name"}, categories: { $push: {category_name:"$category_name",_id:"$_id"} } } }
]
)
Reference:
MongoDB Aggregation
Hope this works.

Categories