MongoDB aggregrate two collections on foreign key - mongodb

I am attempting to pull data from my Mongo instance that will show all users for a given account Id. Users can be part of more than one account so I have currently got this structure for my Mongo models:
UserModel:
username: {
type: String,
required: true,
unique: true,
lowercase: true
},
name: {
type: String,
required: true
},
password: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true
},
UsersToAccountModel:
{
user: {
type: Schema.Types.ObjectId,
ref: 'users'
},
userGroup: {
type: Schema.Types.ObjectId,
ref: 'userGroups'
},
account: {
type: Schema.Types.ObjectId,
ref: 'accounts'
},
default: {
type: Boolean,
default: null
}
}
The UsersToAccount model collection holds the ObjectId of the user, account, and userGroup in it's fields so is acting as a link collection as such.
For every userId that matches the given account ID in the UsersToAccount collection I want to take that ID and query the users table and return it. In MYSQL the query would be:
SELECT * FROM userToAccounts u2a LEFT JOIN users u ON u.id = u2a.userId WHERE u2a.account = $accountId
Can anyone help me here?I have tired aggregation but I am not getting very far.
Here is my attempt so far which isn't working:
const users = await this.userToAccountModel.aggregate(
{
$match: { account: requestVars.account },
},
{
$lookup : { from: "users", localField: "_id", as: "userData", foreignField: "user"}
}
);
Thanks

Firstly, the $lookup stage has the following syntax:
{
$lookup: {
from: <collection to join>,
localField: <field from the input documents>,
foreignField: <field from the documents of the "from" collection>,
as: <output array field>
}
}
So I think you need to swap the localField and foreignField values.
Secondly, aggregate() expect an array.
const users = await this.userToAccountModel.aggregate([
{
$match: { account: requestVars.account },
},
{
$lookup : { from: "users", localField: "user", foreignField: "_id", as: "userData" }
}
]);

Related

Perform share calculations in MongoDB aggregation

I have an Order schema, like so:
const orderSchema = new mongoose.Schema({
order_items: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'OrderItem',
required: true
}],
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
}
total_price: {
type: Number
}
});
And the OrderItems contains purchased products, like so:
const orderItemSchema = new mongoose.Schema({
product_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Product',
required: true
},
quantity: {
type: Number,
required: true
}
});
And the Product schema like so:
const productSchema = new mongoose.Schema({
name: {
type: Map,
of: String,
required: true
},
thumbnail: {
type: String,
default: ''
},
unit_price: {
type: Number,
required: true
}
});
I'm trying to get the share of each purchased product from the total price of the order.
I tried the following:
const totalSales = await Order.aggregate([
{
$lookup: {
from: "orderitems",
localField: "order_items",
foreignField: "_id",
as: "order_items"
}
},
{
$lookup: {
from: "products",
localField: "order_items.product_id",
foreignField: "_id",
as: "products",
pipeline: []
}
},
{
$project: {
order_items: 0,
products: { $divide: ['$products.unit_price', '$total_price'] }
}
}
]);
But I got the following error in postman:
Invalid $project :: caused by :: Cannot use expression other than
$meta in exclusion projection
So, how can I get the desired output?
Thanks
Edit:
I removed order_items: 0 from the project, and now I got this error message:
PlanExecutor error during aggregation :: caused by :: $divide only
supports numeric types, not array and int
From the documentation,
If you specify the exclusion of a field other than _id, you cannot
employ any other $project specification forms.
You should remove order_items: 0, because it is anyway not included in the output.

Automatic data exclusion in aggregations with Mongoose

In my Mongoose models, I've set sensitive data to select: false so it isn't returned with queries by default:
const userSchema = new mongoose.Schema<User, UserModelType, {}, UserQueryHelpers>({
username: {...},
email: { type: String, required: true, unique: true, validate: [validator.isEmail, 'invalid email'], select: false },
password: { type: String, required: true, select: false },
profilePicUrl: { type: String },
emailVerified: { type: Boolean, required: true, default: false, select: false },
[...]
});
To my shock, I realized that this doesn't work in all scenarios. Namely lookup queries in aggregations don't respect select: false.
const result = await KarmaLogEntry.aggregate()
.match({
createdAt: { $gte: startOfCurrentMonth }
})
.group({
_id: "$user",
totalPoints: { $sum: "$points" }
})
.sort({ 'totalPoints': -1 })
.limit(10)
.lookup({
from: "users",
localField: "_id",
foreignField: "_id",
as: "user",
})
.unwind('$user')
.exec();
This returns all the sensitive fields!
I know that I can exclude fields with an additional $project stage:
.lookup({
from: "users",
localField: "_id",
foreignField: "_id",
as: "user",
pipeline: [
{
$project: {
username: '$username',
displayName: '$displayName',
profilePicUrl: '$profilePicUrl',
}
}
]
})
However, if I ever forget to add this for an endpoint, I will leak sensitive user data. Is there a way to apply select: false to all forms of MongoDB queries, including aggregation and lookups?

Getting list of users that referenced product model

const productSchema = mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User',
},
name: {
type: String,
required: true,
},
image: {
type: String,
required: true,
},
price: {
type: Number,
required: true,
default: 0,
},
},
{
timestamps: true,
}
);
const Product = mongoose.model("Product", productSchema);
module.exports = Product;
Hello, Please is it possible to get list or count of users that referenced the product model from the product route or model? i know i get get it through the user route but can i do it from the product route? thanks
Product.aggregate([
{
"$lookup": {
from: "User", /// name of user collection
"localField": "user",
"foreignField": "_id",
"as": "user"
}
},
])
this will add user object as user in product doc as array, so you could have all data of user here and list of them

How can I use the $match operator twice in a single mongoDB query?

I have two models:
const ClientRequest = new mongoose.Schema({
sourceLanguage: {
type: String,
default: '',
trim: true
},
type: {
type: String,
default: '',
trim: true
},
customer: {
type: Schema.Types.ObjectId, ref: 'Client'
}
}
and
const Client = new mongoose.Schema({
name: {
type: String,
default: '',
trim: true
},
web: {
type: String,
default: '',
trim: true
},
country: {
type: String,
default: '',
trim: true
}
}
And I need to find all requests filtered by sourceLanguage and name.
I'm using this query:
const requests = await ClientRequest.aggregate([
{$match: {
"sourceLanguage.symbol": "EN-GB"}
},
{
$lookup: {
from: "clients",
localField: "customer",
foreignField: "_id",
as: "clients"
}
},
{
$match: {
"clients.name": filters.clientFilter,
}
}
])
But it returns empty array. If I remove one of the $match it works. But how can I use both of the filters at the same time in a single query?
const requests = await ClientRequest.aggregate([
{$match: {
"sourceLanguage": "EN-GB",
"customer": ObjectId("5d933c4b8dd2942a17fca425")
}
},
{
$lookup: {
from: "clients",
localField: "customer",
foreignField: "_id",
as: "clients"
}
},
])
I tried different approaches, but as sometimes it happens, the simpliest way worked out:
const requests = await ClientRequest.aggregate([
{
$lookup: {
from: "clients",
localField: "customer",
foreignField: "_id",
as: "customer" // I used the same name to replace the Id with the unwinded object
}
},
{
$match: {
"customer.name": filters.clientFilter,
"sourceLanguage.symbol": "EN-GB" // or any other filter
}
},
{$unwind: "$customer"} // here I just unwind from array to an object
])

Finding parent documents based on a child ID

Suppose I have the following schemas
let TutorialQuizSchema = new mongoose.Schema({
groups: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Group' }]
});
let GroupSchema = new mongoose.Schema({
members: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
responses: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Response' }]
});
let UserSchema = new mongoose.Schema({
name: {
first: String,
last: String
}
});
Given a user ID, is it possible to query all the tutorial quizzes with all the groups that have the user as one of its members?
I'm new to aggregation, but I think it would be something like this
TutorialQuiz.aggreggate([
{
$unwind: '$groups'
},
{
$lookup: {
from: 'groups',
localField: 'groups',
foreignField: '_id',
as: 'group'
}
},
{
$unwind: '$group'
},
{
$match: {
'group.members': { $in: [req.user._id] }
}
}
]).exec((err, data) => {
// etc
})
If I am correct, my only problem with this is that the data comes out flattened. Is it possible to unflatten it to maintain the hierarchical structure (like if we were just doing a find + populate query) ?
Note: if there is better/easier way to do this, I am open to suggestions also.
You can $lookup objectIds directly without $unwinding and use $filter instead of second $unwind inside the $addFields stage to filter the group on presence of user_id value in the members array in 3.4 version.
Something like
TutorialQuiz.aggreggate([
{
$lookup: {
from: 'groups',
localField: 'groups',
foreignField: '_id',
as: 'group'
}
},
{
$addFields: {
group: {
$filter: {
input: "$group",
as: "group",
cond: { $in: [ req.user._id, "$$group.members" ] }
}
}
}
}
])