Mongoose $lookup aggregate doesn't work as expected - node.js

I am writing this mean stack application and here I have a company schema and vehicle schema for that company.
company details schema
var CompanyDetailsSchema = new Schema({
companyName: String,
createdAt: { type: Date, default: Date.now },
...............
...............
});
module.exports = mongoose.model('Company-details', CompanyDetailsSchema);
vehicle schema
var VehicleDetailsSchema = new Schema({
companyId:{
type:mongoose.Schema.ObjectId,
ref: 'Company-details'
},
createdAt: { type: Date, default: Date.now },
....................
....................
});
module.exports = mongoose.model('Vehicle-details', VehicleDetailsSchema);
what I need is to get all the company details also when I get vehicle details. like in an SQL join query, for that I am using this $lookup aggregate. this code sample returns all the vehicles but the company details are empty. how to get all the company details also in mongoose.
getVehciles:() =>{
return new Promise((resolve, reject) => {
VehicleDetailsSchema.aggregate([{
$lookup: {
from: "Company-details", // collection name in db
localField: "companyId",
foreignField: "_id",
as: "companyDetails"
}
}]).exec(function(err, vehicles){
if(err) {
reject(err)
} else {
resolve(vehicles)
}
});
})
},

try this code works for me.
getVehciles:() =>{
return new Promise((resolve, reject) => {
VehicleDetailsSchema.aggregate([{
$lookup: {
from: "company-details", // pass the callection name in small letters
localField: "companyId",
foreignField: "_id",
as: "companyDetails"
}
}]).exec(function(err, vehicles){
if(err) {
reject(err)
} else {
resolve(vehicles)
}
});
})
},

Related

Mongoose: Aggregation return empty value

I'm using aggregation to join 3 collections. But the result of the join is an empty array[].
This is my model for all 3 collections along with queries for aggregate.
Result of console.log(timetable) return []. I;m using references to refer a field in Timetable with corresponding collection.
Timetable Models
var TimetableSchema = new mongoose.Schema ({
timeslot: {
required: true,
'type': String,
},
classroom :{
type: mongoose.Schema.Types.ObjectId,
ref: 'Classroom'
},
subject :{
type: mongoose.Schema.Types.ObjectId,
ref: 'Subject'
},
teacher :{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
day :{
type:String,
required: true,
},
year :{
type:String,
required: true,
},
session :{
type:String,
required:true,
}
})
Classroom Models
var ClassroomSchema = new mongoose.Schema ({
classroom_name: {
type:String,
required:true,
unique: true,
},
classroom_blok:{
type:String,
required:true,
},
classroom_floor: {
type:String,
required:true,
},
});
Subject
var SubjectSchema = new mongoose.Schema ({
subject_id: {
required: true,
'type': String,
'default': shortid.generate
},
subject_name: {
type:String,
required:true,
},
subject_darjah:{
type:String,
required:true,
}
});
Agregate query
router.get('/today_absentee/view/:id',function(req,res){
Teacher.findById(req.session.userId).exec(function (error, user){
if (error){
return next(error);
}else
{
Teacher.find({_id:req.params.id }).exec(function(err, teacher){
if(err)
{
return next(err);
}else
{
Timetable.aggregate([
{
// This is doing the same thing as the previous .find()
$match: { teacher:req.params.id}
},
{
$lookup:{
from: "Classroom", // other table name
localField: "classroom", // name of users table field
foreignField: "_id", // name of userinfo table field
as: "classroom" // alias for userinfo table
}
},
{ $unwind:"$classroom" }, // $unwind used for getting data in object or for one record only
{
$lookup:{
from: "Subject", // other table name
localField: "subject", // name of users table field
foreignField: "_id", // name of userinfo table field
as: "subject" // alias for userinfo table
}
},
{ $unwind:"$subject" }, // $unwind used for getting data in object or for one record only
// define some conditions here
{
$match:{
$and:[{teacher:req.params.id}]
}
},
// define which fields are you want to fetch
{
$project:{
subject_name : "$subject.subject_name",
classroom : "$classroom.classroom_name",
}
}
]).exec(function(err, timetable)
{
// The query output is such that `classroom.classroom_name`
// value is unique for each document
if (err) throw err;
console.log(currentYear);
console.log(timetable);
res.render('creator_content/today_absentee_id',{timetable:timetable, user:user, teacher:teacher});
});
}
});
}
});
});
Models for all 3 collection.
Import mongoose at the top of your file and then use mongoose.Types.ObjectId() when matching against any object id fields in an aggregate query.
I have also used the $addFields aggregation pipeline to add the 2 fields (subject_name, classroom), and then output them in the next stage using $project.
const mongoose = require("mongoose");
router.get('/today_absentee/view/:id', function(req, res) {
Teacher.findById(req.session.userId).exec(function(error, user) {
if (error) {
return next(error);
} else {
Teacher.find({
_id: req.params.id
}).exec(function(err, teacher) {
if (err) {
return next(err);
} else {
Timetable.aggregate([
{
// This is doing the same thing as the previous .find()
$match: {
teacher: mongoose.Types.ObjectId(req.params.id)
}
},
{
$lookup: {
from: "Classroom", // other table name
localField: "classroom", // name of users table field
foreignField: "_id", // name of userinfo table field
as: "classroom" // alias for userinfo table
}
},
{
$unwind: "$classroom"
}, // $unwind used for getting data in object or for one record only
{
$lookup: {
from: "Subject", // other table name
localField: "subject", // name of users table field
foreignField: "_id", // name of userinfo table field
as: "subject" // alias for userinfo table
}
},
{
$unwind: "$subject"
}, // $unwind used for getting data in object or for one record only
// define some conditions here
{
$match: {
$and: [{
teacher: mongoose.Types.ObjectId(req.params.id)
}]
}
},
// Add new field names
{
$addFields: {
subject_name: "$subject.subject_name",
classroom: "$classroom.classroom_name",
}
},
// define which fields are you want to fetch
{
$project: {
subject_name: 1,
classroom: 1,
}
}
]).exec(function(err, timetable) {
// The query output is such that `classroom.classroom_name`
// value is unique for each document
if (err) throw err;
console.log(currentYear);
console.log(timetable);
res.render('creator_content/today_absentee_id', {
timetable: timetable,
user: user,
teacher: teacher
});
});
}
});
}
});
});

Mongoose create child and associated parent at same time

So basically, in my application I have a employee, and a company model. This is just the basic information about these models, there is actually more information, so using nested objects rather than 2 schema's doesn't seem like a good option (I think)
var EmployeeSchema = new Schema(
{
name: { type: String, required: true, max: 100 },
company: { type: Schema.Types.ObjectId, ref: 'Company', required: true },
}
);
var CompanySchema = new Schema(
{
name: { type: String, required: true },
},
{
toJSON: { virtuals: true },
},
);
CompanySchema.virtual('employees', {
ref: 'Employee',
localField: '_id',
foreignField: 'company',
justOne: false,
});
And on a form to create a new employee, I want the option to either select a company, or create a new one.
So my API will send information like the following:
employee: {
name: 'John Bastien',
company: 5d44635fa5993c0424da8e07
}
or:
employee: {
name: 'Dan Smith',
company: {
name: 'ACME'
}
}
This of course can be changed, it was just what I had in mind.
So in my express app when I do var employee = await new Employee(req.body.employee).save(); How can I make it so that the company is created along with the employee. It works fine when sending an object ID, but how can I do it with just a JSON object for the associated document?
I ended up writing some middleware on my models that will handle this. This logic could be extracted out to make it more generic, but for my use case it hasn't needed to yet.
EmployeeSchema.virtual('company', {
ref: 'Company',
localField: 'companyId',
foreignField: '_id',
justOne: true,
}).set(function(company) {
this.companyId= company._id;
this.$company = company;
return this.$company;
});
EmployeeSchema.pre('validate', function(next) {
if (this.company && this.company instanceof Company) {
var err = this.company.validateSync();
if (err) {
// mergeErrors is a helper function that will merge the two exceptions into a nice format
err = mergeErrors(this.validateSync(), { company: err });
}
next(err);
}
next();
});
EmployeeSchema.pre('save', async function(next, saveOpts) {
if (this.company && this.company instanceof Company && this.company.isModified()) {
await this.company.save(saveOpts);
}
next();
});

Mongoose - How to populate a sub document on condition

I'm new to Mongoose I don't know how to populate on condition.
So this is my model :
const OrderSchema = new Schema({
products: [{ type: Schema.Types.ObjectId, ref: 'Product' }],
remarks: {type: String, lowercase: true}
});
mongoose.model("Order", OrderSchema);
const ProductSchema = new Schema({
reference: {type: String}
status: {type: Schema.Types.ObjectId, ref: 'ProductStatus'}
});
mongoose.model("Product", ProductSchema);
const ProductStatus = new Schema({
name: {type: String}
});
const CountrySchema = new Schema({
name: {type: String}
});
mongoose.model("Country", CountrySchema);
I have a getOrderById methods
export const getOrderById = async (req, res) => {
let id = req.params.id;
try {
await orderModel
.findById(id)
.populate({
path: 'products',
populate: {
path: 'country',
model: 'Country'
}
})
.populate({
path: 'products',
populate: {
path: 'status',
model: 'ProductStatus'
}
})
.exec(function (err, orders) {
if (err) {
res.send(err);
}
res.status(200).json(orders);
});
} catch (error) {
console.log(error);
}
}
And now I would like to show in the order lists all products that have the status Received in France.
First, I guess you also missed reference to the country in the product schema, so assuming these are your corrected schemas:
const OrderSchema = new Schema({
products: [{
type: Schema.Types.ObjectId,
ref: 'Product'
}],
remarks: {
type: String,
lowercase: true
}
});
const Order = mongoose.model("Order", OrderSchema);
const ProductSchema = new Schema({
reference: {
type: String
},
country: {
type: Schema.Types.ObjectId,
ref: 'Country'
},
status: {
type: Schema.Types.ObjectId,
ref: 'ProductStatus'
}
});
const Product = mongoose.model("Product", ProductSchema);
const ProductStatusSchema = new Schema({
name: {
type: String
}
});
const ProductStatus = mongoose.model("ProductStatus", ProductStatusSchema);
const CountrySchema = new Schema({
name: {
type: String
}
});
const Country = mongoose.model("Country", CountrySchema);
As far as I understand you want to only show the products, whose country's name is 'France' and ProductStatus' name is 'Received', these kind of operations are done through Aggregation
Your query may look like this assuming you want to do it one query:
const getOrderById = async (req, res) => {
let id = req.params.id.toString();
const ObjectId = mongoose.Types.ObjectId
try {
const aggregationStages = [{
$match: {
_id: ObjectId(id) //It is important to cast to ObjectId in aggregations
}
}, {
$lookup: {
from: 'products',
let: {
productIds: '$products'
},
pipeline: [{
$match: {
$expr: {
$in: ['$_id', '$$productIds']
}
}
}, {
$lookup: {
from: 'countries',
localField: 'country',
foreignField: '_id',
as: 'country'
}
}, {
$lookup: {
from: 'productstatuses',
localField: 'status',
foreignField: '_id',
as: 'status'
}
}, {
$match: {
'country.name': 'France',
'status.name': 'Received'
}
}],
as: 'products'
}
}];
await orderModel.aggregate(aggregationStages)
.exec(function (err, orders) { // The return is an array btw.
if (err) {
res.send(err);
}
res.status(200).json(orders);
});
} catch (error) {
console.log(error);
}
}
If you feel the aggregation is complicated you may resort to breaking it to smaller simpler queries. Feel free to ask if you need more explanation/modification.

Aggregation issue with 2 collections

The purpose of this is to fetch A pro of Type Secretaire from a Cabinet with a specified name (in this case "Clinique Toto") and I'm struggling here.
Cab Model:
var cabinet = new cabModel({
_id: new mongoose.Types.ObjectId(),
InfoCab:{Nom: "Clinique Toto"} //This is the Name of the Cabinet
});
cabinet.save((err, cabinet) => {
Pro Model
var pro1 = new proModel({
_id: new mongoose.Types.ObjectId(),
Nom: 'ProTITI',
Cv:{ Fonction: { Secretaire: false}}
});
pro1.Cabinets.push(cabinet._id);
pro1.save((err, cabinet) => { });
var pro2 = new proModel({
_id: new mongoose.Types.ObjectId(),
Nom: 'Pro_TOT',
Cv:{ Fonction: { Secretaire: true}}
});
Setting Secretaire: true for some of the Pros.
pro2.Cabinets.push(cabinet._id);
pro2.save((err, cabinet) => { });
var pro3 = new proModel({
_id: new mongoose.Types.ObjectId(),
Nom: 'Josianne',
Cv:{ Fonction: { Secretaire: true}}
});
pro3.Cabinets.push(cabinet._id);
pro3.save((err, cabinet) => { });
Pushing Pros created into the Cab.
cabinet.Pro.push(pro1, pro2, pro3);
cabinet.save();
console.log("Done");
});
const handleError = function (err) {
console.error(err);
};
I got to this so far:
db.Pro.aggregate([
{
$match: {
Cv: {
Fonction: {
Secretaire: true
}
}
}
},
{
$lookup:
{
from: "Cab",
localField:"Nom",
foreignField: "_id",
as: "PK"
}
}
])
Here are the Schemas:
Pro Schema:
const ProSchema = new Schema({
_id: { type: Schema.Types.ObjectId },
Cv: {Fonction: {Pro: {type: Boolean,},
Secretaire: {type: Boolean}
}
}
CabSchema:
const CabSchema = new Schema({
Pro: [{ type: Schema.Types.ObjectId, ref: 'ProSchema' }],
InfoCab: {
Nom: {type: String}
});
Can you add Schema for your models so your question has more clarification.
From the given information, it looks like Nom is a string, i.e. Nom: 'Josianne' and you are using lookup as follows:
$lookup:
{
from: "Cab",
localField:"Nom",
foreignField: "_id",
as: "PK"
}
Now the problem is _id is of type ObjectId(), which is a hash string uniquely generated, where Nom is a string created by you, logically they will never match.
iF Cab collection is same as cabModel, the foreignField should be InfoCab.Nom. i.e. foreignField: "InfoCab.Nom",
=== UPDATE ===
Couple of observations you might wana consider:
you should use the aggregation as following: proModel.aggregate([...])
If you are already using ref in your mongoose schema, you can use .populate() method.

Reference not populated

In a User schema, I have a simple reference to a Customer schema.
const UserSchema = new Schema({
customer: { type: Schema.Types.ObjectId, ref: Customer }, // Customer is the compiled CustomerSchema
...
});
const CustomerSchema = new Schema({
name: String,
...
});
In an Express controller, I'm fetching an user and I'm trying to embed the customer in the returned JSON:
export function me(req, res, next) {
User
.findOne({ _id: req.user._id }, '-salt -hashedPassword')
.populate('customer')
.exec((err, user) => {
if(err) return next(err);
if(!user) return res.json(401);
res.json(user);
});
}
But in the response, customer is null.
The test data I use:
A user document:
{
"_id" : ObjectId("570d1f0938f7da5151b815d2"),
"customer" : ObjectId("570d1f0838f7da5151b815d0"),
...
}
The related customer document:
{
"_id" : ObjectId("570d1f0838f7da5151b815d0"),
...
}
Probably a noob question, but I don't see what I don't see what I could forget =)
I think ref must be a string:
customer: { type: Schema.Types.ObjectId, ref: 'Customer' },

Resources