Mongoose: Aggregation return empty value - node.js

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
});
});
}
});
}
});
});

Related

NodeJs, Mongoose find and populate works but aggregate, match and lookup doesn't

I am having trouble with my aggregation.. I need to return the list of the items which have the same owner's id. Owner's ID I pass as a query param.. I have tried already $match without the $in,
I tried transforming my id to mongoose.Types.ObjectId (with this I event get an error). But yet I accomlpished only empty object as return..
exports.getContainersByOwner = async (req: Request, res: Response) => {
const { id }: any = req.query;
try {
const containers = await Container.aggregate([
{
$lookup: {
from: "containerstypes",
localField: "containerTypeID",
foreignField: "_id",
as: "containerTypes",
},
},
{
$lookup: {
from: "owners",
localField: "ownerId",
foreignField: "_id",
as: "owner",
},
},
{ $unwind: "$containerTypes" },
{ $unwind: "$owner" },
{ $match: { "owner._id": { $in: [id] } } },
]);
res.status(200).json(containers);
} catch (err) {
res.status(404).json({ success: false, msg: "Container not found" });
}
};
If I remove line $match from my code api returns all the items which is OK, but I need to achieve that the returned items have the same id... I need to match the colored id (owner._id)
The Container's model is:
const ContainerSchema = new mongoose.Schema({
ownerId: { type: Schema.Types.ObjectId, ref: "Owners", required: true },
manufacturingNo: { type: String, required: true },
IdentificationNo: { type: String, required: true },
manufacturingDate: { type: Date, required: true },
nextInspectionDate: { type: Date, required: true },
certificateNo: { type: String, required: false },
containerTypeID: {
type: Schema.Types.ObjectId,
ref: "ContainersType",
required: true,
},
});
if I use this approach it works, but object structure is not right, so in frontend there is much more changes to do.. This is not possible at the moment
try {
const containers = await Container.find({
ownerId: id,
})
.populate("containerTypeID")
.populate("ownerId");
res.status(200).json(containers);
} catch (err) {
res.status(404).json({ success: false, msg: "Container not found" });
}
Your $match is logically wrong:
{ $match: { "owner._id": { $in: [id] } } }
Since id, itself is a list of Ids, your $in becomes list of lists ([[]]). Try simply this:
{ $match: { "owner._id": { $in: id } } }

NodeJS and MongoDB - use aggregate and $lookup together with findById

I want to make a relation between two collections - a book and author collections.
If i use only get and display all of my books and integrate the data about author by id it works.
Author schema:
const AuthorSchema = new mongoose.Schema({
name: { type: String, required: true },
surname: { type: String, required: true },
dateOfBirth: { type: String, required: true },
countryOfBirth: { type: String, required: true },
});
Book schema:
const BookSchema = new mongoose.Schema({
owner: { type: String, required: true },
pagesNo: { type: String, required: true },
releaseDate: { type: String, required: true },
country: { type: String, required: true },
authorID: { type: Schema.Types.ObjectId, ref: "Author", required: true }, <-- HERE I NEED DATA ABOUT AUTHOR
});
My express function that works for fetching all data:
router.get("/", async (req, res) => {
try {
let books = await Book.aggregate([
{
$lookup: {
from: "authors",
localField: "authorID",
foreignField: "_id",
as: "author",
},
},
]);
res.status(200).json(books);
} catch (err) {
res.status(404).json({ success: false, msg: "Book is not found" });
}
});
But now I want to display that joined data also when i search for a single book by ID (findById()).
I got an error status if I use a function like this:
router.get("/:bookId", async (req, res) => {
try {
let book= await Book.aggregate([
{
$lookup: {
from: "authors",
localField: "authorID",
foreignField: "_id",
as: "author",
},
},
]);
book= book.findById({ _id: req.params.bookId});
res.status(200).json(book);
} catch (err) {
res.status(404).json({ success: false, msg: "Book is not found" });
}
});
Thank you for your help
use the $match to find only one book for the same query
const mongoose = require('mongoose');
const ObjectId = mongoose.Types.ObjectId();
router.get("/:bookId", async (req, res) => {
try {
let book= await Book.aggregate([
{
$match: { _id : ObjectId("book _id") }
},
{
$lookup: {
from: "authors",
localField: "authorID",
foreignField: "_id",
as: "author",
},
},
]);
book= book.findById({ _id: req.params.bookId});
res.status(200).json(book);
} catch (err) {
res.status(404).json({ success: false, msg: "Book is not found" });
}
});

Mongoose $lookup aggregate doesn't work as expected

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)
}
});
})
},

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();
});

Populate from two collections

I have 2 Schemas defined for 2 different collections, and I need to populate one of them into the other:
stationmodel.js
var stationSchema = new Schema({
StationName: 'string',
_id: 'number',
Tripcount: [{ type: Schema.Types.ObjectId, ref: 'Tripcount'}]
},
{collection: 'stations'}
);
module.exports = mongoose.model('Station', stationSchema);
tripmodel.js
var tripSchema = new Schema({
_id: { type: Number, ref: 'Station'},
Tripcount: 'number'
},
{collection: 'trips'}
);
module.exports = mongoose.model('Tripcount', tripSchema);
According to the mongoose populate documentation, this is the way to go. I have the problem that "Tripcount" remains as [] when I use Postman to GET the stations.
My DB Structure for the 'stations' collection:
{
"_id": 1,
"StationName": "Station A",
}
And for the 'trips' collection:
{
"_id": 1,
"Tripcount": 6
}
My routes.js:
module.exports = function(app) {
app.get('/stations', function(req,res) {
var query = Station.find().populate('Tripcount');
query.exec(function(err, stations){
if(err)
res.send(err);
res.json(stations);
});
});
};
I can't seem to find the error, maybe someone here can spot a mistake I made.
You are enclosing the mongoose SchemaTypes in single quotes, you either need to reference the SchemaTypes directly when you define a property in your documents which will be cast to its associated SchemaType.
For example, when you define the Tripcount in the tripSchema it should be cast to the Number SchemaType as
var tripSchema = new Schema({
_id: Number,
Tripcount: Number
}, {collection: 'trips'});
module.exports = mongoose.model('Tripcount', tripSchema);
and the station schema
var stationSchema = new Schema({
_id: Number,
StationName: String,
Tripcount: [{ type: Number, ref: 'Tripcount'}]
}, {collection: 'stations'});
module.exports = mongoose.model('Station', stationSchema);
Then in your stations collection, the documents would ideally have the structure
{
"_id": 1,
"StationName": "Station A",
"Tripcount": [1]
}
for the populate method to work, of which when applied as
Station.find().populate('Tripcount').exec(function(err, docs){
if (err) throw err;
console.log(docs);
// prints { "_id": 1, "StationName": "Station A", "Tripcount": [{"_id": 1, Tripcount: 6 }] }
});
Alternative Approach
Another approach that you could take if the station collection does not have Tripcount field is to use the $lookup operator found in the aggregation framework as:
Station.aggregate([
{
"$lookup": {
"from": "tripcollection",
"localField": "_id",
"foreignField": "_id",
"as": "trips"
}
},
{
"$project": {
"StationName": 1,
"trips": { "$arrayElemAt": ["$trips", 0] }
}
},
{
"$project": {
"StationName": 1,
"Tripcount": "$trips.Tripcount"
}
}
]).exec(function(err, docs){
if (err) throw err;
console.log(docs);
// prints [{ "_id": 1, "StationName": "Station A", "Tripcount": 6 }] }
});

Resources