Mongoose find by field in inner object (ObjectID) - node.js

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"} ]
}

Related

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.

Empty array output from the $lookup aggregation operator

I'm trying to get the most popular recipes.
I have 2 relevant collections: recipes and favoriterecipes.
Things to consider: I've double checked collections names and ids.
In the db itself the _recipe field in favoriterecipes is type string and in recipes it is an ObjectId. (maybe a type conversion is required? even though I didn't see such thing in "lookup" examples).
favoriteRecipe.js:
const mongoose = require("mongoose");
const Recipe = require("./recipe");
const User = require("./user");
const FavoritesRecipesSchema = new mongoose.Schema({
_recipe: { type: mongoose.Schema.Types.ObjectId, ref: Recipe },
_user: { type: mongoose.Schema.Types.ObjectId, ref: User },
});
module.exports = mongoose.model("FavoriteRecipe", FavoritesRecipesSchema);
recipe.js:
const mongoose = require("mongoose");
const User = require("./user");
const RecipeScheme = new mongoose.Schema({
name: String,
ingredients: [String],
instructions: String,
image: String,
date: { type: Date, default: Date.now },
tags: [String],
_user: { type: mongoose.Schema.Types.ObjectId, ref: User },
});
module.exports = mongoose.model("Recipe", RecipeScheme);
controller.js:
exports.popular = async function (req, res, next) {
try {
const popular_recipes = await favoriteRecipe.aggregate([
{
$group: {
_id: "$_recipe",
recipeCount: { $sum: 1 },
},
},
{ $sort: { recipeCount: -1 } },
{
$lookup: {
from: "recipes",
localField: "_id",
foreignField: "_id",
as: "recipe",
},
},
// { $unwind: "$recipe" },
// {
// $project: {
// _id: "$recipe",
// recipeCount: 1,
// },
// },
]);
res.json(popular_recipes);
} catch (error) {
next(error);
}
};
response output:
[
{
"_id": "6053349353b5f5632986b2c2",
"recipeCount": 3,
"recipe": []
},
{
"_id": "6053349353b5f5632986b2c3",
"recipeCount": 2,
"recipe": []
},
{
"_id": "605603945b4aeb0d2458153e",
"recipeCount": 1,
"recipe": []
}
]
Eventually I found that I have to convert the id string to an object id. the solution:
exports.popular = async function (req, res, next) {
try {
const popular_recipes = await favoriteRecipe.aggregate([
{
$group: {
_id: { $toObjectId: "$_recipe" },
recipeCount: { $sum: 1 },
},
},
{ $sort: { recipeCount: -1 } },
{
$lookup: {
from: "recipes",
localField: "_id",
foreignField: "_id",
as: "recipe",
},
},
{ $unwind: "$recipe" },
{
$project: {
_id: "$recipe",
recipeCount: 1,
},
},
]);
res.json(popular_recipes);
} catch (error) {
next(error);
}
};

MongoDB, populate nested object in array

I have a question ...
I have a schema like this :
const chatSchema = new Schema({
[...]
title: {
type: String,
required: true
},
message: [
{
content: {
type: String,
required: true
},
creator: {
type: mongoose.Types.ObjectId,
required: true,
ref: 'User'
}
}
],
[...]
});
in my Node backend, how can I have access to my creators instances inside my message array ? I don’t know how to write the query with populate ...
Thank you all ! :-)
use the following command:
...after importing chatSchema as maybe chats
module.exports.populateCreator = (req, res) => {
chats.findOne({chatID})
.populate({
path: 'message',
populate: {
path: "creator"
}
})
.then(chats => res.json(chats))
}
You can use like this
Chats.find(query)
.populate({
path: 'message',
populate: {
path: 'creator'
}
})
.exec(function(err, docs) {});
the query you are looking for is:
https://mongoplayground.net/p/2dpeZWsXR-V
db.booking.aggregate([
{
"$match": {
id: "61fdfeef678791001880da25"
}
},
{
$unwind: "$cart"
},
{
"$lookup": {
"from": "products",
"localField": "cart.product",
"foreignField": "id",
"as": "prod"
}
},
{
"$unwind": "$prod"
},
{
"$project": {
id: 1,
status: 1,
cart: [
{
id: "$cart.id",
date: "$cart.date",
timeSlots: "$cart.timeSlots",
product: {
id: "$prod.id",
name: "$prod.name",
}
}
],
}
}
])

How to get array of objects from current collection and data from another collection in same query

I have a users collection. Each user can have multiple friends whose _id values are stored in an array in their user document.
I want to render a list which has a user's list of friend names and under each individual name I want to have a location but the locations are in another document because each user can have multiple locations with no limit on the number, like their visited/favourite places.
These are the 2 queries in isolation but I need to somehow combine them.
This is the query which looks at the user ID values in the user's array of friends and then gets the relevant user info based on the ID.
friends = await User.findOne({ _id: userId })
.populate("friends", "name mobile", null, { sort: { name: 1 } })
.exec();
Then, to get the places for a particular user:
const place = await Place.find({ creator: userId });
So, I basically want to list the friends in a loop, each with their places under their name like:
Joe Soap
- London Bridge
- Eiffel Tower
Bob Builder
- Marienplatz
The data in mongoDb look like this:
Users:
{
"_id": {
"$oid": "5f2d9ec5053d4a754d6790e8"
},
"friends": [{
"$oid": "5f2da51e053e4a754d5790ec"
}, {
"$oid": "5f2da52e053d4a754d6790ed"
}],
"name": "Bob",
"email": "bob#gmail.com",
"created_at": {
"$date": "2020-08-07T18:34:45.781Z"
}
}
Places:
{
"_id": {
"$oid": "5f3695d79864bd6c7c94e38a"
},
"location": {
"latitude": -12.345678,
"longitude": 12.345678
},
"creator": {
"$oid": "5f2da51e053e4a754d5790ec"
},
}
The first join is basically getting data from the array inside the same user collection. The second one is getting data from another collection, places.
UPDATE: almost working
friends = await User.aggregate([
{ $match: { _id: new mongoose.Types.ObjectId(userId) } },
{
$lookup: {
from: "users",
localField: "friends",
foreignField: "_id",
as: "friends_names",
},
},
{ $unwind: "$friends_names" },
{
$lookup: {
from: "places",
localField: "friends",
foreignField: "creator",
as: "friends_places",
},
},
{ $unwind: "$friends_places" },
{
$project: {
"friends_names.name": 1,
"friends_places.saved_at": 1,
},
},
]);
Data returned:
[
{
_id: 5f2d9ec5053d4a754d6790e8,
friends_names: { name: 'Bob' },
friends_places: { saved_at: 2020-08-17T13:40:28.334Z }
},
{
_id: 5f2d9ec5053d4a754d6790e8,
friends_names: { name: 'Natalie' },
friends_places: { saved_at: 2020-08-17T13:40:28.334Z }
}
]
Edit Okay, I believe I have reverse engineered your minimal schema correctly:
const mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/test", { useNewUrlParser: true });
mongoose.set("debug", true);
const db = mongoose.connection;
db.on("error", console.error.bind(console, "connection error:"));
db.once("open", async function () {
await mongoose.connection.db.dropDatabase();
// we're connected!
console.log("Connected");
const userSchema = new mongoose.Schema({
friends: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }],
name: String,
});
const placesSchema = new mongoose.Schema({
latitude: String,
creator: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
});
const User = mongoose.model("User", userSchema);
const Place = mongoose.model("Place", placesSchema);
const bob = new User({ name: "Bob", friends: [] });
await bob.save();
const natalie = new User({ name: "Natalie", friends: [bob] });
await natalie.save();
//const chris = new User({ name: "Chris", friends: [] });
//await chris.save();
const john = new User({ name: "John", friends: [natalie, bob] });
await john.save();
const place1 = new Place({ latitude: "Place1", creator: bob });
const place3 = new Place({ latitude: "Place3", creator: bob });
const place2 = new Place({ latitude: "Place2", creator: natalie });
await place1.save();
await place2.save();
await place3.save();
await User.find(function (err, users) {
if (err) return console.error(err);
console.log(users);
});
await Place.find(function (err, places) {
if (err) return console.error(err);
//console.log(places);
});
var cc = await mongoose
.model("User")
.aggregate([
{ $match: { _id: john._id } },
{ $unwind: "$friends" },
{
$lookup: {
from: "places",
localField: "friends",
foreignField: "creator",
as: "friends_places",
}
}, { $lookup: {from: 'users', localField: 'friends', foreignField: '_id', as: 'friend_name'} },//, { $include: 'friends' }
{ $unwind: "$friends_places" }, { $unwind: "$friend_name" }//, { $skip: 1}, {$limit: 1}
])
.exec();
console.log(cc);
});
This is the most relevant part:
var cc = await mongoose
.model("User")
.aggregate([
{ $match: { _id: john._id } },
{ $unwind: "$friends" },
{
$lookup: {
from: "places",
localField: "friends",
foreignField: "creator",
as: "friends_places",
}
}, { $lookup: {from: 'users', localField: 'friends', foreignField: '_id', as: 'friend_name'} },//, { $include: 'friends' }
{ $unwind: "$friends_places" }, { $unwind: "$friend_name" }//, { $skip: 1}, {$limit: 1}
])
.exec();
The first unwind: friends 'flattens' the collections initially. So, basically we've got 'userId | friendId' for each user and friend. Then, for each row, we simply look up places created by him ($lookup). Finally, we unwind friends_places because we don't want them to be rendered as [object] in console output. Additionally, there this $match, because we only want to check one user's friends' places. Considering we want to know friend's name as well, we have to do one more join - this is why there's this second $lookup. After that a simple $unwind to get friend's detail and that's it.
Code yields the following:
[ { _id: 5f3aa3a9c6140e3344c78a45,
friends: 5f3aa3a9c6140e3344c78a44,
name: 'John',
__v: 0,
friends_places:
{ _id: 5f3aa3a9c6140e3344c78a48,
latitude: 'Place2',
creator: 5f3aa3a9c6140e3344c78a44,
__v: 0 },
friend_name:
{ _id: 5f3aa3a9c6140e3344c78a44,
friends: [Array],
name: 'Natalie',
__v: 0 } },
{ _id: 5f3aa3a9c6140e3344c78a45,
friends: 5f3aa3a9c6140e3344c78a43,
name: 'John',
__v: 0,
friends_places:
{ _id: 5f3aa3a9c6140e3344c78a46,
latitude: 'Place1',
creator: 5f3aa3a9c6140e3344c78a43,
__v: 0 },
friend_name:
{ _id: 5f3aa3a9c6140e3344c78a43,
friends: [],
name: 'Bob',
__v: 0 } },
{ _id: 5f3aa3a9c6140e3344c78a45,
friends: 5f3aa3a9c6140e3344c78a43,
name: 'John',
__v: 0,
friends_places:
{ _id: 5f3aa3a9c6140e3344c78a47,
latitude: 'Place3',
creator: 5f3aa3a9c6140e3344c78a43,
__v: 0 },
friend_name:
{ _id: 5f3aa3a9c6140e3344c78a43,
friends: [],
name: 'Bob',
__v: 0 } } ]
So, we've got a flat list of John's friends' places.
Important bit: from: places in $lookup is critical, as we have to use MongoDB's name of the collection, not model name 1.

Multiple condition with $lookup aggregation

I am looking at using best practices with the following problem with Mongoose.
I've got three schemas:
const SchemaA = new Schema({
name: String
});
const SchemaB = new Schema({
schemaA: {
type: Schema.Types.ObjectId,
ref: 'SchemaA',
required: true
}
});
const SchemaC = new Schema({
schemaB: {
type: Schema.Types.ObjectId,
ref: 'SchemaB'
},
user: {
type: Schema.Types.ObjectId,
ref: 'User'
}
});
I need to get schemaB objects by schemaA id with schemaC attached, but filtered by user.
getSchemaBs: async (schemaAId, userId) => {
try {
SchemaB.find({ schemaA: schemaAId }).populate('schemaC': where user === userId);
} catch (e) {
throw new Error('An error occurred while getting schemasB for specified schemaA.');
};
I'm in the middle of refactoring of the code written with use of Mongo native driver for NodeJS. Now I want to make it simpler with Mongoose usage.
Earlier version of the code (keep in mind it could not follow best practices):
getList: function (schemaAId, userId) {
return new Promise(
function (resolve, reject) {
db.collection('schemaB').aggregate([{
$match: {
'isDeleted': false,
'schemaA': ObjectID(schemaAId)
}
},
{
$lookup: {
from: "schemaC",
localField: "_id",
foreignField: "schemaBId",
as: "schemasC"
},
},
{
$project: {
_id: true,
schemaAId: true,
// other neccessary fields with true (several lines - makes code ugly and messy)
schemasC: {
"$arrayElemAt": [{
$filter: {
input: "$schamasC",
as: "schemaC",
cond: {
$eq: ["$$schemaC.userId", ObjectID(userId)]
}
}
}, 0]
}
}
}
]).toArray(function (error, result) {
if (error) {
reject(error);
} else {
resolve(result);
};
});
});
}
How can I deal with that the best way?
What you are trying to do can be better done by using mongodb 3.6 $lookup syntax which filters the document inside the $lookup pipeline
db.collection('schemaB').aggregate([
{ "$match": { "isDeleted": false, "schemaA": ObjectID(schemaAId) }},
{ "$lookup": {
"from": "schemaC",
"let": { "schemaBId": "$_id" },
"pipeline": [
{ "$match": {
"$expr": { "$eq": ["$schemaBId", "$$schemaBId"] },
"userId": ObjectID("5b5c747d8209982630bbffe5")
}}
],
"as": "schemasC"
}}
])

Resources