Matching one of two fields with $in and $elemMatch - node.js

Combining $in and $elemMatch
I'm trying to find a partial match for an employee's first or last name. I tried using the $or operator, but Mongo doesn't seem to recognize it.
Where am I going wrong?
router.get('/employees', async (req, res, next) => {
if(req.query.name) {
const response = await Companies.find({ employees:
{
$elemMatch: {
$in: [
{ first_name:new RegExp(req.query.name, 'i') },
{ last_name:new RegExp(req.query.name, 'i') }
]
}
}
})
res.json({ response })
}
else { next() }
})
My schema looks like this:
const companies = new mongoose.Schema({
company: [{
name: { type:String, required:true },
contact_email: { type:String, required:true },
employees: [{
first_name:String,
last_name:String,
preferred_name:String,
position: { type:String },
birthday: { type:Date },
email: { type:String },
}],
}]
}, {
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }
})
I have some names in there that should match (including just "nna"), but sending a get request to /employees?name=nna just returns an empty response array.

the most convenient way to get employees back would be to unwind and then match like so:
db.Companies.aggregate({
"$unwind": "$employees"
}, {
"$project": {
"employees": "$employees",
"_id": 0
}
}, {
"$match": {
"$or": [
{
"employees.first_name": /anna/i
},
{
"employees.last_name": /anna/i
}
]
}
})
on the other hand, if the goal is to get the companies back, then this would work:
db.Companies.find({
"employees": {
"$elemMatch": {
"$or": [
{
"first_name": /anna/i
},
{
"last_name": /anna/i
}
]
}
}
})

Related

Mongoose find by field in inner object (ObjectID)

For example I have two models
const User = new Schema({
name: { type: String },
});
const Message = new Schema({
user: { type: ObjectId, ref: 'User' },
message: { type: String },
});
How to find messages by the key "name" in the user?
It doesn't work
exports.get = async (req, res) => {
return Message.find({ "user.name": req.query.name })
.populate('user', 'name')
.then((data) => res.json(data));
}
I understand why "user.name" doesn't work, but I don't know how to solve it
You would want to use $lookup -> unwind -> match:
Unwind: convert user array to user object from look up stage
exports.get = async (req, res) => {
return Message.aggregate([
{
$lookup: {
from: "users",
localField: "user",
foreignField: "_id",
as: "user",
},
},
{
$unwind: {
path: "$user",
},
},
{
$match: {
"user.name": req.query.name,
},
},
])
.then((data) => res.json(data));
}
There is no concept of a join in Mongodb. Hence I would recommend using straight schema definition like this:
const User = new Schema({
name: { type: String },
});
const Message = new Schema({
user: [User],
message: { type: String },
});
then you wouldn't need to use populate anymore. Instead, you would have something like this:
Message.find({ "user.name": req.query.name })
However, If you still prefer to use the current approach, You can try this:
Message.find()
.populate({
path: "user",
match: {
name: req.query.name
}
})
...
Basically you need the correct ordering of table
User must be your primary table and Messages must be your secondary table from which you should lookup.
The query is as following;
exports.get = async (req, res) => {
const queryLength = req.query.name ? req.query.name.length : 0;
return User.aggregate([
{
$match: {
name: {
"$regex": req.query.name,
"$options": '-i'
}
},
$lookup: {
let: { id: _id },
from: 'Messages',
pipeline: [
{
$match: {
$expr: { $eq: ["$user", "$$id"] }
}
},
{
$project: {
user: 0,
_id: 0,
}
}
],
as: "Messages"
},
$project: {
regex_match: {
$eq: [
req.query.name,
{ $substr: [{ $toLower: "$name" }, 0, queryLength] }
]
},
Messages: 1
},
$sort: {
regex_match: -1//we use regex match to get the most match result on the top like if u search "Al" then all results with al in them will be on top
},
$project: {
Messages: 1,
regex_match: 0
}
}
])
.then((data) => res.json(data));
}
This will return all the messages as an array with field name messages of "name" person e.g;
{
Messages: [{_id: 1234, message: "Hello"},{_id: 12345, message: "Hi"} ]
}

Join two or more queries in mongo db node.js and get result as a single object using aggregate query

I have two collections as follows:
import mongoose from "mongoose";
const projectSchema = mongoose.Schema({
id: String,
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
title: String,
details: String,
location: String,
rate: String,
status: {
type: String,
default: "active",
},
createdAt: {
type: Date,
default: new Date(),
},
});
const Project = mongoose.model("Project", projectSchema);
export default Project;
import mongoose from "mongoose";
const proposalSchema = mongoose.Schema({
id: String,
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
projectId: { type: mongoose.Schema.Types.ObjectId, ref: "Project" },
rate: String,
message: String,
createdAt: {
type: Date,
default: new Date(),
},
});
const Proposal = mongoose.model("Proposal", proposalSchema);
export default Proposal;
And in response to a GET request, I want to get all the projects which are active and user has not sent the proposal to them, GET request will have the id of user.
(Proposal: When a user sends a proposal, a proposal object is created in proposals collections which has userId and ProjectId)
I have make it work using the below queries but it doesn't looks efficient and good. Is there a way I can get this result using aggregate query or any better way from this?
And also how I can efficiently can convert objectId to string Id here.
export const getProjects = async (req, res) => {
try {
const activeProjects = await Project.find({ status: "active" }, { _id: 1 });
const projectsWithProposals = await Proposal.find(
{
$and: [
{ userId: req.query.id },
{ projectId: { $in: activeProjects } },
],
},
{ _id: 0, projectId: 1 }
);
const stringsIds = projectsWithProposals.map((id) =>
id.projectId.toString()
);
const projects = await Project.find({
$and: [{ status: "active" }, { _id: { $nin: stringsIds } }],
});
res.status(200).json(projects);
} catch (error) {
res.status(404).json({ message: error.message });
}
};
Here is a aggregation function which delivers all Projects which have no proposal from a given user:
function getQ (userId) {
return [
{
"$match": {
"$expr": {
"$eq": [
"$status",
"active"
]
}
}
},
{
"$lookup": {
"from": "proposals",
"localField": "_id",
"foreignField": "projectId",
"as": "proposals"
}
},
{
"$set": {
"uids": "$proposals.userId"
}
},
{
"$unset": "proposals"
},
{
"$match": {
"$expr": {
"$not": [
{
"$in": [
mongoose.Types.ObjectId(userId),
"$uids"
]
}
]
}
}
},
{
"$unset": "uids"
},
{
"$limit": 10
}
]
}
db.Project.aggregate(getQ("62a61df204f2ce244ce0ffcc")) // input is user._id
.then(console.log)
.catch(console.error)
I have used the standard mongoose _ids so you might have to adapt the code if required.
The query does only output the Project collection data, although it would be easy to include other data as well.
Beware that limit is constant here. You could also convert skip and limit to function paramters which would make the function much more flexible if you are working with huge amounts of results.

$lookup with condition in mongoose

I have 2 schemas, this is parent collection schema:
const TimesheetSchema = Schema({
managersComment: {
type: String,
},
weekNum: {
type: Number,
},
year: {
type: Number,
},
user: { type: Schema.Types.ObjectId, ref: userModel },
status: {
type: String,
enum: ["Saved", "Submitted", "Approved", "Rejected"],
},
data: [{ type: Schema.Types.ObjectId, ref: TimesheetIndividualData }]
});
This is child collection schema
const TimesheetDataSchema = new Schema(
{
workingDate: {
type: Date,
},
dayVal: {
type: Number,
},
user: { type: Schema.Types.ObjectId, ref: userModel },
parentId: { type: String }
},
{ timestamps: true }
);
In TimesheetDataSchema parentId is basically the _id from TimesheetSchema.
Now i need to run a query which return docs from TimesheetDataSchema, but only the docs in which parentId(ObjectId) of TimesheetSchema has status Approved.
I am trying to do $lookup, but currently no success. Please help.
EDIT: Based upon #ashh suggestion tried this: but getting empty array.
const result = await TimesheetIndividualData.aggregate([
{
"$lookup": {
"from": "timesheetModel",
"let": { "parentId": "$parentId" },
"pipeline": [
{ "$match": { "status": "Approved", "$expr": { "$eq": ["$weekNum", "$parentId"] } } },
],
"as": "timesheet"
}
},
{ "$match": { "timesheet": { "$ne": [] } } }
])
You can use below aggregation
const result = await db.TimesheetDataSchema.aggregate([
{ "$lookup": {
"from": "TimesheetSchema",
"let": { "parentId": "$parentId" },
"pipeline": [
{ "$match": { "status": "approved", "$expr": { "$eq": ["$_id", "$$parentId"] }}},
],
"as": "timesheet"
}},
{ "$match": { "timesheet": { "$ne": [] }} }
])
But I would prefer two queries for better performance here
const timesheets = (await db.TimesheetSchema.find({ status: "approved" }, { _id: 1 })).map(({ _id }) => _id)
const result = await db.TimesheetDataSchema.find({ parentId: { $in: timesheets } })

Delete an object from an array in Express Mongoose

{
"_id": "608c3d353f94ae40aff1dec4",
"userId": "608425c08a3f8db8845bee84",
"experiences": [
{
"designation": "Manager",
"_id": "609197056bd0ea09eee9429c"
},
{
"designation": "Asst. Manager",
"_id": "608c530de8ade5221b0e6d4e"
},
{
"designation": "Sr. Manager",
"_id": "608c534be8ade5221b0e6d4f"
},
]
}
I want to delete object in array with id 608c530de8ade5221b0e6d4e, here is my code but that gives me error.
This is the controller:
const userId = req.userData.userId;
const id = req.params.id;
Experience.findOneAndUpdate({ userId: userId }, { $pull: { 'experiences': { '_id': id } }}, { multi:true }, function(err, obj) {
// //do something here
});
This is model:
const newExpSchema = new mongoose.Schema({
designation: { type: String, default: ""},
});
const experienceSchema = new mongoose.Schema({
userId: { type: String, required: true },
experiences: [newExpSchema],
});
export default model("experience", experienceSchema);
I am getting below error on { $pull: { 'experiences': { '_id': id } }}
Error:
No overload matches this call.
Overload 1 of 3 .........
.......
The expected type comes from property '$pull' which is declared here on type 'UpdateQuery<Document<any, {}>>'
Can you try this:
Folder.findOneAndUpdate({
"_id": "608c3d353f94ae40aff1dec4"
},
{
$pull: {
"experiences": {
"_id": "608c530de8ade5221b0e6d4e"
}
}
},
{
"multi": false
})
Here is a working example: https://mongoplayground.net/p/YtNGBTr52U9

How can i use MONGODB aggregation framework with nested external document - Mongoose and NodeJS

I have these Mongoose schemes:
// User schema
exports.User = new Schema({
name: {
type: String,
required: true
},
home: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Post'
}]
});
// Post schema
exports.Post = new Schema({
likes: [{
type: Schema.Types.ObjectId,
ref: 'User'
}],
author: {
id: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
name: {
type: String,
required: true
},
shortId: String, // this is User id
},
created: {
type: Date,
default: Date.now,
required: true
}
});
// THE DATA IN THE DATABASE
// User
{"name" : "Mio Nome",
"home" : [
ObjectId("533af14b994c3647c5c97338")
]}
// Post
{ "author" : {
"id" : ObjectId("533af14b994c3647c5c97338"),
"name" : "AutoreDelPost"
},
"likes" : [
ObjectId("533af14b994c3647c5c97337"),
ObjectId("533af14b994c3647c5c97339"),
ObjectId("533af14b994c3647c5c97340")
]
}
And i want to get from users the posts in home field and count how many likehave one user
With this code i can show all posts in home whit populate, but i can't count likes.
req.db.User.find({
_id: req.user._id //req.user is my test user
}, {
home: 1
})
.limit(200)
.populate('home')
.exec(function (err) {
if (!err) {
return res.json(200)
}
return res.json(500, err)
});
// output
[
{
"_id": "533af0ae994c3647c5c97337",
"name" : "Mio Nome"
"home": [
{
"_id": "533b004e6bcb9105d535597e",
"author": {
"id": "533af14b994c3647c5c97338",
"name": "AutoreDelPost"
},
"likes": [] // i can't see like and i can't count they
}
]
I tryed to use aggregate, to count etc but i can't see the posts getting populated but their _id
req.db.User.aggregate({
$match: {
_id: req.user._id
}
}, {
$project: {
home: 1
}
}, {
$unwind: "$home"
}).exec(function (err, home) {
if (!err) {
return res.json(200, home)
}
return res.json(500, err)
});
// output
[
{
"_id": "533af0ae994c3647c5c97337",
"home": "533b004e6bcb9105d535597e"
},
{
"_id": "533af0ae994c3647c5c97337",
"home": "533b004e6bcb9105d5355980"
},
{
"_id": "533af0ae994c3647c5c97337",
"home": "533b004f6bcb9105d5355982"
},
{
"_id": "533af0ae994c3647c5c97337",
"home": "533b004f6bcb9105d5355984"
},
{
"_id": "533af0ae994c3647c5c97337",
"home": "533b00506bcb9105d5355986"
}
]
QUESTION: I want to get from users the posts in home field and count how many like a user has
Perhaps you can store your data more denormalized and add a counter field which is incremented on each new "like". See http://cookbook.mongodb.org/patterns/votes/. Something like:
update = {'$push': {'voters': user_id}, '$inc': {vote_count: 1}}

Resources