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"
}}
])
Related
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"} ]
}
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.
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);
}
};
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
}
]
}
}
})
I have this kind of 'comment' model:
{ _id: <comment-id>,
user: {
id: { type: Schema.ObjectId, ref: 'User', required: true },
name: String
},
sharedToUsers: [{ type: Schema.ObjectId, ref: 'User' }],
repliedToUsers: [{ type: Schema.ObjectId, ref: 'User' }],
}
And I want to query for all comments which pass the following conditions:
sharedToUsers array is empty
repliedToUsers array is empty
But also, I want the result to contain only 1 comment (the latest comment) per user by the user id.
I've tried to create this aggregate (Node.js, mongoose):
Comment.aggregate(
{ $match: { "sharedToUsers": [], "repliedToUsers": [] } },
{
$group: {
_id: "$user.id",
user: { $first: "$user" },
}
},
function (err, result) {
console.log(result);
if (!err) {
res.send(result);
} else {
res.status(500).send({err: err});
}
});
It is actually working, but the serious problem is that the results comments _id field is been overwritten by the nested user _id.
How can I keep the aggregate working but not to overwrite the original comment _id field?
Thanks
Ok, I have a solution.
All I wanted is to group by _id but to return the result documents with their _id field (which is been overwritten when using $group operator).
What I did is like wdberkley said, I've added comment_id : { "$first" : "$_id" } but then I wanted not to return the comment_id field (because it doesn't fit my model) so I've created a $project which put the comment_id in the regular _id field.
This is basically how it looks:
Comment.aggregate(
{
$match: {
"sharedToUsers": [], "repliedToUsers": []
}
},
{
$group: {
comment_id: { $last: "$_id" },
_id: "$user.id",
content: { $last: "$content" },
urlId: { $last: "$urlId" },
user: { $last: "$user" }
}
},
{
$project: {
_id: "$comment_id",
content: "$content",
urlId: "$urlId",
user: "$user"
}
},
{ $skip: parsedFromIndex },
{ $limit: (parsedNumOfComments - parsedFromIndex) },
function (err, result) {
console.log(result);
if (!err) {
Comment.populate(result, { path: "urlId"}, function(err, comments) {
if (!err) {
res.send(comments);
} else {
res.status(500).send({err: err});
}
});
} else {
res.status(500).send({err: err});
}
});
thanks wdbkerkley!