mongoose .findById & .find return object & array, causes compatibility issue - mongodb

When the user visits a certain page of my App, the Component dispatches an action to fetch information. Namely, the action performs the following operations:
Base.find({req.params.id})
BaseUser.find({ baseId: req.params.id }) **returns multiple docs**
Message.find({ baseId: req.params.id }) **returns multiple docs**
The operation happens in this order. I could query the first one via .findById, but for uniformity of the problem I chose .find(). The problem now is that the results of
Promise.all([
Base.find({ _id: req.params.id }),
BaseUser.find({ baseId: req.params.id }),
Message.find({ baseId: req.params.id })
])
come in an array, like so:
[
[ { created: 2018-08-29T23:59:35.380Z,
_id: 5b8741151985662f10d04fdb,
creatorId: 5b86f7970cd98b2004969bf0,
title: 'testBase1',
} ],
[ { created: 2018-08-30T00:57:57.764Z,
acceptedMembership: true,
isCreator: true,
_id: 5b8741151985662f10d04fdc,
userId: 'tester1',
baseId: 5b8741151985662f10d04fdb }
],
[ { created: 2018-08-30T00:58:09.182Z,
_id: 5b8741211985662f10d04fdd,
baseId: 5b8741151985662f10d04fdb,
content: 'testMessage1' }
]
]
This quite obviously causes problems when further trying to map/filter/res.json() the data. Is there any known way to return this in a single array, or even better, pass it to the front-end (redux action) as an object? Does anyone know of a better solution which handles this problem slightly differently, and prevents me from fetching each of those methods on subcomponents ?
update:
I have now constructed this, which is fairly ugly to look at:
let completeObject = {
base: {},
users: [],
messages: []
};
Base.findById(req.params.id)
.then(data => {
completeObject.base = data;
return data;
})
.then(data => {
BaseUser.find({ baseId: req.params.id })
.then(data => {
completeObject.users = data;
return data;
})
.then(data => {
Message.find({ baseId: req.params.id }).then(data => {
completeObject.messages = data;
return res.json(completeObject);
});
});
})

Why don't you setup ref in the Base model to the BaseUser and Message and then use populate to fill those arrays and get one object as result filled with the arrays of BaseUser and Message?
From what I see you key on the req.params.id which means you have a cross-reference between those collections anyway.
Here is an example:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var BaseSchema = Schema({
_id: Schema.Types.ObjectId,
creatorId: Schema.Types.ObjectId,
title: String,
users: [{ type: Schema.Types.ObjectId, ref: 'User' }],
messages: [{ type: Schema.Types.ObjectId, ref: 'Message' }],
});
var UserSchema = Schema({
_id: Schema.Types.ObjectId,
acceptedMembership: Boolean,
isCreator: Boolean,
userId: String,
baseId: Schema.Types.ObjectId
});
var MessageSchema = Schema({
_id: Schema.Types.ObjectId,
baseId: Schema.Types.ObjectId,
content: String
});
var Base = mongoose.model('Base', BaseSchema);
var User = mongoose.model('User', UserSchema);
var Message = mongoose.model('Message', MessageSchema);
Now that the schemas are defined (and you added some records) you could find a Base by _id and populate users and messages:
Base.
findOne({ _id: req.params.id }).
populate('users').
populate('messages').
exec(function (err, base) {
if (err) return handleError(err);
console.log(base);
});
You should check the mongoose documentation on how to save / populate references etc.

Related

How can I see the products per each category with mongoose

this is my schema for storing products using mongoose as below.
const mongoose = require("mongoose");
const mongoosePaginate = require("mongoose-paginate-v2");
const productSchema = mongoose.Schema({
_id: mongoose.Schema.Types.ObjectId,
name: {
type: String,
required: true,
},
category: {
type: mongoose.Schema.Types.ObjectId,
ref: "Category",
},
productImage: {
type: String,
required: true,
},
description: {
type: String,
},
createdAt: {
type: Date,
default: new Date(),
},
deletedAt: {
type: Date,
},
});
productSchema.plugin(mongoosePaginate);
const productModel = mongoose.model("Product", productSchema, "Product");
module.exports = productModel;
and this how I have the schema for storing categories that products are related to
const mongoose = require("mongoose");
const categorySchema = mongoose.Schema({
_id: mongoose.Schema.Types.ObjectId,
product: { type: mongoose.Schema.Types.ObjectId, ref: "Product" },
});
const categoryModel = mongoose.model("Category", categorySchema, "Category");
module.exports = categoryModel;
What I don´t know is how to populate my controller.
getAll: async (req, res) => {
const limitPage = parseInt(req.query.limit, 10) || 10;
const pageChange = parseInt(req.query.page, 10) || 1;
Product.paginate({}, { limit: limitPage, page: pageChange })
.then((result) => {
return res.status(200).json({
message: "GET request to all getAllProducts",
dataCount: result.length,
result: result,
});
})
.catch((err) => {
console.log(err);
res.status(500).json({
error: err,
});
});
},
Please help, I don´t understand why it not being populated and how to see the categories displayed with the categorie they belong to.
You should probably include populate in your query like so:
...
Product.paginate({}, { limit: limitPage, page: pageChange }).populate('category')
...
Note: Are you sure you want to have a 1-1 relation between products and categories. Because this is what you achieve if you set the relation like you did on both schemas. If yes, you should find a way to ensure that this 1-1 relation is enforced each time you save or update objects.

findOneAndUpdate document with array

Two questions here.
What is the correct way to findOneAndUpdate when there is an array? The example below errors with err MongooseError [CastError]: Cast to embedded failed for value.
Should you arrays of objects become separate collections?
* Example *
var ProductSchema = new Schema({
_id: Schema.Types.ObjectId,
product_name: String
});
var purchaseOrderSchema = new Schema(
{
purchaseOrderNo: Number,
products: [ProductSchema]
}
);
const purchaseOrder = new PurchaseOrder(req.body);
PurchaseOrder.findOneAndUpdate(
{ _id: req.body._id },
{
$set: req.body,
$push: req.body.products
},
{ upsert: true, new: true }
)
.then((result) => {
console.log('result', result);
res.status(200).json(result);
})
.catch((err) => {
console.log('err', err);
res.status(500).json({ error: err });
});
const body = {
_id: 'skjdhflksjdf',
purchaseOrderNo: 1,
products: [
{
_id: '111',
product_name: 'Cup'
},
{
_id: '222',
product_name: 'Spoon'
}
]
}
In the ProductSchema the type of _id field to set to ObjectId. The product id 111 and 222 are not a valid ObjectId and it fails to cast it. You can update the type of _id in ProductSchema to Number for this to work
var ProductSchema = new Schema({
_id: Number,
product_name: String
});

Load nested virtual during mongodb query

I'm new to using a key other than ObjectId to link data from other collections. Currently, I have appointments with various other data I'd like to bring in so I can evaluate whether payment is due or not.
My query worked, except it doesn't bring in the plan information for each patient. I understand that it makes a separate query for each populate, so I'd have to do it after I populate the patient information with populate('patientID'):
const appts = await Appt.find(searchParams)
.populate('patientID')
.populate('patientID.plan')
.populate('status')
.populate('type')
.sort({ scheduled: -1 });
The above doesn't work for bringing in the nested JSON of the plan information, but it DOES work for bringing in the patient collection, status, and type. Only patientID.plan populate doesn't work.
My schemas:
const familySchema = new mongoose.Schema({
ID: {
type: Number,
index: true
},
family: String
});
const paymentplanSchema = new mongoose.Schema({
ID: {
type: Number,
index: true
},
plan: String,
planamt: Number
});
const patientSchema = new mongoose.Schema({
ID: {
type: Number
},
familyID: Number,
first: String,
last: String,
careplanID: Number,
otherData: variousTypes
});
patientSchema.virtual('plan', {
ref: 'PaymentPlan', // The model to use
localField: 'careplanID', // Find people where `localField`
foreignField: 'ID' // is equal to `foreignField`
});
patientSchema.pre('find', function() {
this.populate('plan');
});
const typeSchema = new mongoose.Schema({
ID: Number,
appttype: String,
abbr: String,
amt: Number,
code: String,
length: Number
});
const statusSchema = new mongoose.Schema({
ID: Number,
status: String
});
const apptSchema = new mongoose.Schema({
ID: Number,
patientID: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Patient'
},
oldPatientID: Number,
status: {
type: mongoose.Schema.Types.ObjectId,
ref: 'ApptStatus'
},
type: {
type: mongoose.Schema.Types.ObjectId,
ref: 'ApptType'
},
scheduled: Date,
note: String
});
mongoose.model('Appt', apptSchema);
mongoose.model('ApptStatus', statusSchema);
mongoose.model('ApptType', typeSchema);
mongoose.model('Patient', patientSchema);
mongoose.model('PaymentPlan', paymentplanSchema);
How do I get the patient data to load WITH the plan data? I don't get what I'm doing wrong, and I've got other things I'd like to connect in this way (via index instead of ObjectId) but just don't get what I'm doing wrong.
UPDATED TO ADD MORE DETAIL:
My query on the backend to get the appointments is this:
module.exports.search = async (req, res) => {
console.log('GET the appts');
const searchParams =
req.params.query === 'today'
? { scheduled: { $gt: new Date(dayStart), $lt: new Date(dayEnd) } }
: req.body;
console.log(searchParams);
try {
const appts = await Appt.find(searchParams)
.populate({
path: 'patientID',
populate: { path: 'plan' }
})
.populate('status')
.populate('type')
.sort({ scheduled: -1 });
if (!appts) {
console.log(`No appointments found`);
}
appts.forEach(p => {
const patient = p.patientID ? p.patientID.nickname : 'NONE';
const plan =
p.patientID && p.patientID.plan ? p.patientID.plan.planamt : 0;
console.log(patient, plan);
});
console.log(appts.length, 'appts found');
res.send(appts);
} catch (err) {
console.log(`Error`, err);
return res.status(500).send(err);
}
};
In the console, It's logging correctly (example):
CarF 60
8075 'appts found'
In the frontend, all the objects are populated EXCEPT patientID.plan. The patientID object does not include a plan field on any of the entries. patientID, status, and type all populated the corresponding objects.
WHY is this logging on the backend, but not visible on the frontend?
You should be able to do it by passing a path option to populate():
const appts = await Appt.find(searchParams)
.populate('patientID')
.populate({
path: 'patientID',
populate: {path: 'plan'}
})
.populate('status')
.populate('type')
.sort({ scheduled: -1 });
See https://mongoosejs.com/docs/populate.html#deep-populate in official docs

Saving a document in Mongoose, reference id is not stored in the second document

When I save a new "experience" document with the model Experience, the experience _id is not saved into the document of the user. So my "experiences" array in the user document remains empty. Why?
const mongoose = require('mongoose');
const ExperienceSchema = mongoose.Schema({
name: String,
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
reviews: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Review' }],
categories: [{ type: String }],
});
module.exports = mongoose.model('Experience', ExperienceSchema);
==============================================
const mongoose = require('mongoose');
const UserSchema = mongoose.Schema({
name: String,
experiences: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Experience' }],
});
module.exports = mongoose.model('User', UserSchema);
=============================================
// Update experience to database
router.post('/:id', (req, res, next) => {
const idexp = req.params.id;
const newExperience = {
name: req.body.name,
user: req.user._id,
};
Experience.findOneAndUpdate({ _id: idexp }, newExperience, (err, result) => {
if (err) {
return res.render(`/${idexp}/edit`, { errors: newExperience.errors });
}
return res.redirect(`/experiences/${idexp}`);
});
});
The experiences is the sub-document of user schema. So, when you save experiences, the user will not be saved. However, when you save user, the experience should be saved.
Refer this subdocs documentation
Here is the solution... I needed to use $push to update the user document with the experience id before rendering the site.
Experience.findOneAndUpdate({ _id: idexp }, newExperience, (err, result) => {
if (err) {
return res.render('experiences/edit', { errors: newExperience.errors });
}
User.findByIdAndUpdate({ _id: req.session.passport.user._id }, { $push: { experiences: idexp } }, (err) => {
if (err) {
next(err);
} else {
return res.redirect(`/experiences/${idexp}`);
}
});
});

Populate does not retrieve the whole referenced object just the ids

I've been reading a few answers regarding this and yet I still can't get it to work.
My model objects aren't deeply nested and are quite simple. It's events that have a list of users attending them and users that have a list of events they've attended. like so:
let DinnerSchema = new mongoose.Schema({
date: {
type: Date,
unique: true,
timestamps: true,
required: true
},
title:{type: String, require: true},
attending: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}]
})
and the users:
let UserSchema = new mongoose.Schema({
email: {
type: String,
lowercase: true,
unique: true,
required: true
},
name:{ type: String, require: true },
password: {type: String ,required: true},
dinners: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Dinner'
}]
})
And for clarity here's the entire route that's using populate:
userpage.get('/', authCheck, (req, res) => {
const options = { _id: '57ebbf48bd6914036f99acc7' }
return Dinner
.findOne(options)
.populate('User', 'name') //I'VE TRIED ADDING 'name' BASED ON SOME ANSWERS I SAW
.exec((err, newDinner) => {
if (err) {
console.log(err)
res.status(400).end()
}
console.log(newDinner) // SHOW'S USERS ID'S BUT NO OTHER FIELDS
return res.json({
sucsess: true,
data: newDinner
})
})
})
If I understand correctly in the database itself there should only be a reference to the other model and not actually all of it's fields and the join happens with the populate. My db structure show's just the reference so that's ok.
I've tried specifying the name of the fields i'm after (the name field in this case) but that didn't work.
My population result always looks like the following and doesn't show any other fields except for the _id one:
{
_id: 57ebbf48bd6914036f99acc7,
date: 2016-09-27T22:00:00.000Z,
title: '1',
__v: 0,
attending: [ 57ebbcf02c39997f9cf26891, 57ebbdee098d3c0163db9905 ]
}
What am I screwing up here?
In mongoose populate receives 4 parameters.
path
selection(fields to be return) ,
condition
options (like {limit:10})
In your case you are not passing right path to populate. It should be
userpage.get('/', authCheck, (req, res) => {
const options = { _id: '57ebbf48bd6914036f99acc7' }
return Dinner
.findOne(options)
.populate('attending', 'name')
.exec((err, newDinner) => {
if (err) {
console.log(err)
res.status(400).end()
}
console.log(newDinner) // SHOW'S USERS ID'S BUT NO OTHER FIELDS
return res.json({
sucsess: true,
data: newDinner
})
})
})
Now it will return all the names of attending users.
you need to populate attending - that's your user reference in the dinner schema