MongoDB creating cross-referenced documents in Mongoose? - mongodb

I am using Mongoose for a content-app that stores exhibition reviews. I decided to use references rather than subdocuments, so each Exhibition stores an array of _Ids for it's Reviews and each Review stores the _Id of it's Exhibition.
I want to require both fields, but the problem I run into is, what order to create them in?
I don't like my work around because I use an empty array for thereviews when creating an Exhibition, then exhibition.reviews.push() if the subsequent Review creation is successful, but have to Exhibition.deleteOne() if it throws an error.
Is this robust or am I risking having Reviews with no Exhibition reference or Exhibitions with no Reviews ?
Review model:
const reviewSchema = mongoose.Schema({
content: {
type: String,
required: true
},
exhibition: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Exhibition',
required: true
}
})
const Review = mongoose.model('Review', reviewSchema)
Exhibition model:
const exhibitionSchema = mongoose.Schema({
title: {
type: String,
required: true
},
reviews: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Review',
required: true
}]
})
const Exhibition = mongoose.model('Exhibition', exhibitionSchema)
My work-around:
try {
const exhibition = await Exhibition.create({
title: 'title'
reviews: []
})
try {
const review = await Review.create({
review: 'review'
exhibition: exhibition._id,
})
exhibition.reviews.push(review._id)
await exhibition.save()
} catch {
Exhibition.deleteOne({ _id: exhibition._id })
}
} catch (err) {
return handleError(err)
}
})
Is there a better way to do this?

Related

How to Find record by ObjectID's property in Mongodb?

Suppose I have 2 models like Orders and Customers.
const orders = mongoose.Schema({
customer_id: {
type: mongoose.Schema.ObjectId
required: true,
ref: 'Customer',
},
OrderNo: {
type: String,
required: true,
})
const customers = mongoose.Schema({
name: {
type: String,
required: true,
})
I want to filter orders based on customer's name as per below.
let query = {
'customer_id.name': { $regex: '.*AMAZON.*' },
}
await Orders.find(query)
but its not working properly. I'm new to mongodb. Can anyone share feasible solution ? Thanks
Try to populate and filter the resulting fields:
await Orders.find({})
.populate({ path: 'customer_id', match: { name: { $regex: '.*AMAZON.*' } } })
.exec();

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

Ensure a unique index on nested reference on a mongoose schema

What I want is that a user can like a post only once, hence I uniquely indexed the user in the likes array to ensure the same, but it isn't working and I can't find out what is wrong here .
The schema looks like this :
const mongoose = require('mongoose')
const postSchema = new mongoose.Schema({
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User' // User model
},
text: {
type: String,
required: [true, 'Post must have some text']
},
likes: [
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}
}
],
comments: [
{
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
text: {
type: String,
required: [true, 'Comment must have some text']
},
addedAt: {
type: Date,
default: Date.now
}
}
],
createdAt: {
type: Date,
default: Date.now
}
})
postSchema.pre(/^find/, function(next) {
this.populate({
path: 'author',
select: 'name avatar'
}).populate({
path: 'comments.author',
select: 'name avatar'
})
next()
})
// Ensure a user can like a post only once
postSchema.index({ 'likes.user': 1 }, { unique: true })
const Post = mongoose.model('Post', postSchema)
module.exports = Post
However when I send a post request to like a post twice via the same user it
shows no error.
Here is the postman output
I have tried both the ways listed in this, but none of them worked in this case.
Mongoose Index on a field in nested document
How do I ensure a user can like a post only once from the schema itself ?
Try saving likes in this format in the database
likes:[{type:mongoose.Schema.Types.ObjectId,ref: 'User'}]
making it
likes:[ObjectId("5af03111967c60501d97781f")]
and when the post like API is hit do
{$addToSet: {likedBy: userId}}
in update query,addToSet ensures no duplicate ids are maintained in the array.

MissingSchemaError while using Mongoose Populate with only one model

**I have answered below. In short you need to require the Model in the module in which you wish to populate, even though you do not refer to it directly.
I am hitting a strange problem with mongoose when populating just one particular array of IDs.
I have three models, User, Company and Widgets.
When I return the company populated with the users all is fine using:
Company.findOne({ name: 'xyz' })
.populate('users')
.exec(function(err, company) {
if (err) return res.send(err)
res.send(company)
})
However when I try to replace populate 'users' with 'widgets' I get the following error:
{
"message": "Schema hasn't been registered for model \"widget\".\nUse mongoose.model(name, schema)",
"name": "MissingSchemaError"
}
Here are the models:
USER:
const UserSchema = new Schema({
name: String,
email: {
type: String,
unique: true,
required: true
},
password: {
type: String,
required: true
},
company: {
type: Schema.Types.ObjectId,
ref: 'company'
}
});
const User = mongoose.model("user", UserSchema);
COMPANY:
const CompanySchema = new Schema({
name: String,
URL: {
type: String,
unique: true
},
users: [{
type: Schema.Types.ObjectId,
ref: 'user'
}],
widgets: [{
type: Schema.Types.ObjectId,
ref: 'widget'
}]
});
const Company = mongoose.model('company', CompanySchema);
WIDGET:
const WidgetSchema = new Schema({
title: {
type: String,
required: true
},
maker: String
});
const Widget = mongoose.model('widget', WidgetSchema);
I have manually inspected the _ids in the widget array of the company model and they are all correct.
OK, so this was a lack of understanding on my behalf.
In the module where I was using:
Company.findOne({ name: 'xyz' })
.populate('users')
.exec(function(err, company) {
if (err) return res.send(err)
res.send(company)
})
I had imported the User model for other uses in the module. However, as I was not directly referring to Widget I had not imported it. Having done some more research I found that you need to import a model when populating even though not referring to it directly.
Let me know if best to delete whole thread or leave for reference.

Does Mongoose Actually Validate the Existence of An Object Id?

I like the validation that comes with Mongoose. We are trying to figure out whether we want to use it, and put up with the overhead. Does anyone know if providing a reference to the parent collection when creating a mongoose schema, (in the child schema, specify the object id of the parent object as a field,) does this then mean that every time you try to save the document it checks the parent collection for the existence of the refereneced object id?
I'm doing it with middleware, performing a search of the element on validation:
ExampleSchema = new mongoose.Schema({
parentId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Example'
}
});
ExampleModel = mongoose.model('Example', ExampleSchema);
ExampleSchema.path('parentId').validate(function (value, respond) {
ExampleModel.findOne({_id: value}, function (err, doc) {
if (err || !doc) {
respond(false);
} else {
respond(true);
}
});
}, 'Example non existent');
I'm using mongoose-id-validator. Works good
var mongoose = require('mongoose');
var idValidator = require('mongoose-id-validator');
var ReferencedModel = new mongoose.Schema({name: String});
var MySchema = new mongoose.Schema({
referencedObj : { type: mongoose.Schema.Types.ObjectId, ref: 'ReferencedModel'},
referencedObjArray: [{ type: mongoose.Schema.Types.ObjectId, ref: 'ReferencedModel' }]
});
MySchema.plugin(idValidator);
No, an ObjectId field that's defined in your schema as a reference to another collection is not checked as existing in the referenced collection on a save. You could do it in Mongoose middleware, if needed.
I found this thread very helpful and this is what I came up with:
This Middleware (I think its one anyway please let me know if not) I wrote checks the referenced model for the id provided in the field.
const mongoose = require('mongoose');
module.exports = (value, respond, modelName) => {
return modelName
.countDocuments({ _id: value })
.exec()
.then(function(count) {
return count > 0;
})
.catch(function(err) {
throw err;
});
};
Example model:
const mongoose = require('mongoose');
const uniqueValidator = require('mongoose-unique-validator');
const Schema = mongoose.Schema;
const User = require('./User');
const Cart = require('./Cart');
const refIsValid = require('../middleware/refIsValid');
const orderSchema = new Schema({
name: { type: String, default: Date.now, unique: true },
customerRef: { type: Schema.Types.ObjectId, required: true },
cartRef: { type: Schema.Types.ObjectId, ref: 'Cart', required: true },
total: { type: Number, default: 0 },
city: { type: String, required: true },
street: { type: String, required: true },
deliveryDate: { type: Date, required: true },
dateCreated: { type: Date, default: Date.now() },
ccLastDigits: { type: String, required: true },
});
orderSchema.path('customerRef').validate((value, respond) => {
return refIsValid(value, respond, User);
}, 'Invalid customerRef.');
orderSchema.path('cartRef').validate((value, respond) => {
return refIsValid(value, respond, Cart);
}, 'Invalid cartRef.');
orderSchema.path('ccLastDigits').validate(function(field) {
return field && field.length === 4;
}, 'Invalid ccLastDigits: must be 4 characters');
orderSchema.plugin(uniqueValidator);
module.exports = mongoose.model('order', orderSchema);
I'm a very new dev so any feedback is greatly valued!
You can try https://www.npmjs.com/package/lackey-mongoose-ref-validator (I'm the developer)
It also prevents deletion if the reference is used on another document.
var mongooseRefValidator = require('lackey-mongoose-ref-validator');
mongoSchema.plugin(mongooseRefValidator, {
onDeleteRestrict: ['tags']
});
It's an early version, so some bugs are expected. Just fill in a ticket if you find any.
I know this is an old thread but I had the same problem and I came up with a more "modern" solution.
I'm not an expert myself, hope I'm not misleading anyone, but this seems to work:
for example, in a simple "notes" schema, which contains a user field:
const noteSchema = new Schema({
user: { type: Schema.Types.ObjectId, ref: 'User' },
text: String
});
here's the middleware that checks if the userId exists:
noteSchema.path('user').validate(async (value) => {
return await User.findById(value);
}, 'User does not exist');