Mongoose Model.find -> Edit -> Callback? - node.js

Sorry for the vague title, but what I'm trying to do is the following:
I've got 2 mongoose Models: posts and users (which can be the author of a post)
const Post = new Schema({
title: {type: String, required: true, unique: true},
content: {type: String, required: true},
date_created: {type: Date, required: true, default: Date.now},
authorId: {type: String, required: true}, // ObjectId
author: {type: Schema.Types.Mixed},
page: {type: Boolean, required: true, default: false}
});
post.find()
mongoose sends query to MongoDB
MongoDB returns documents
Middleware that retrieves the author based on the authorId property
Add found user to the posts author field
post.find callback
Is this possible?

yes, mongoose document references and population will do this for you.
const Post = new Schema({
// ...
author: {type: mongoose.Schema.Types.ObjectId, required: true, ref: "User"}
});
the ref: "User" tells Mongoose to use the "User" type as the object type. Be sure you have a "User" model defined with Mongoose or this will fail.
to load the full object graph, use the populate method of a query:
Post
.findOne(/* ... */)
.populate('author')
.exec(function (err, story) {
// ...
});
P.S. I cover this and more in my MongooseJS Fundamentals screencast package.

Related

Mongoose find documents based on subdocument reference value

I am trying to grab documents based on subdocuments reference.
I have tried
Blog.find({"user.blocked" : false})
that query doesn't pull any documents results.
Here are my Schemas
const BlogSchema = new mongoose.Schema({
category: {type: String, trim: true},
user: {type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true},
title: {type: String},
description: {type: String},
}, {timestamps: true});
const UserSchema = new mongoose.Schema({
name: {type: String, required: true},
blocked: Boolean,
}, {timestamps: true});
mongoose.model('User', UserSchema);
mongoose.model('Blog', BlogSchema);
You are defining user as a reference, so you cannot query on fields of other collection. This is one of the differences with relational Databases, you cannot perform a JOIN.
Your alternative is to use an aggregation instead of a query, using the $lookup operator. You can check about it here

Mongoose validation value from another table

This is my Product table schema
let schema = new mongoose.Schema({
title: {type: String, required: true},
price: {type: Number, required: true},
description: {type: String, required: true},
sizes: {type: Object, required: true},
offer: {type: Number, default: 0},
images: {type: Array, required: true},
deal: {type: Boolean, default: false},
category: {
_id: {type: Schema.Types.ObjectId, required: true},
name: {type: String, required: true}
},
company_name: {type: String, required: true}
});
What I am trying to do
I am trying to validate if category.name value equal exist in my another table called Category.
You could use an async validator and query the categories collection. Something like this (using promise sytax for validator):
let schema = new mongoose.Schema({
title: {type: String, required: true},
price: {type: Number, required: true},
description: {type: String, required: true},
sizes: {type: Object, required: true},
offer: {type: Number, default: 0},
images: {type: Array, required: true},
deal: {type: Boolean, default: false},
category: {
_id: {type: Schema.Types.ObjectId, required: true},
name: {
type: String,
required: true,
validate: function(nameVal) {
return new Promise(function(resolve, reject) {
let Category = mongoose.model('Category'); //you would have to be sure that Category model is loaded before this one. One reason not to do this
Category.findOne({name: nameVal}, (err, cat) => resolve(cat ? true : false)); //any non null value means the category was in the categories collection
});
}
}
},
company_name: {type: String, required: true}
});
Some thoughts about this:
This is documented at http://mongoosejs.com/docs/validation.html#async-custom-validators.
As it states there, the validator is not run by default on updates. There are a lot of caveats to read through there.
In my experience with NoSQL DBs, the code creating a new Product would take care to make sure the category being assigned was valid. The code probably found the category from the DB at some point prior. So having a validator lookup would be redundant. But you may have a situation where you have a lot of code that creates Products and want validation in one place.
When I see that you are storing a document of {_id: ..., name: ...} as the category field in your Product schema, I think you might want this instead:
...
category: {Schema.Types.ObjectId, ref: 'Category'},
This allows you to store a reference to a category you have retrieved from the categories collection. Mongoose will load up the category document inline for you when you retrieve the products, if you use the populate method on your query. See http://mongoosejs.com/docs/populate.html. There are a lot of options with the populate functionality you might find useful. It does not do validation that the category is valid on save, as far as I know. But if you take this approach, you would have already looked up the category previously in the code before your save (see the link to get a better idea what I mean). Essentially this gives you join like behavior with MongoDB, with the storage savings and other benefits one expects from "normalization".

Mongoose result.toObject keep Schema methods

Because I cannot edit properties of a non-lean mongoose result, I've used the result.toObject() statement, but that also means I cannot use the methods defined on my Schema.
Example
// Defining the schema and document methods
const User = new Schema({
email: {type: String, required: true, unique: true},
firstname: {type: String, required: true},
registration_date: {type: Date, default: Date.now, required: true},
insert: {type: String},
lastname: {type: String, required: true}
});
User.methods.whatIsYourFirstName = function () {
return `Hello, my firstname is:${this.firstname}`;
};
After a find:
user = user.toObject();
user.registration_date = moment(user.registration_date);
user.whatIsYourFirstName();
// result in "user.whatIsYourFirstName is not a function"
Is this solvable?
Methods and Models are part of Mongoose, not MongoDB.
Whenever you are calling .toObject() you are being returned an object which is ready for storage in MongoDB.
If you do need to do any sort of value transformation, I'd do it just before you deliver the value to the user. Being a time formatting, if you are building an API, I'd do that in the client; if you are working with templates try transforming the value on the same template.

Can I populate ref's in a mongoose.js model instead of everytime I query?

Intro: I am creating a StackExchange clone using Node and Mongo to learn the language. I am currently working on the API.
I have the following 'questionSchema':
var questionSchema = new Schema({
_id : {type: String, default: shortid.generate},
title : {type: String, required: true},
question : {type: String, required: true},
user : {type: Schema.ObjectId, ref: 'User'},
points : {type: Number, default: 0},
date : {type: Date, default: Date.now},
answers : [answerSchema],
votes : [{
user: {type: Schema.ObjectId, ref: 'User', required: true},
vote: {type: Number, enum: [-1,0,1]}
}],
__v : {type: Number, select: false}
});
The idea is that when a user votes on a question the points field is incremented (or decremented) and the userid and vote added to the votes array. I have the vote array to detect if the user has already voted and prevent additional votes.
The problem: I'm having trouble actually checking if the user has voted (checking if their userid exists in the votes array). I have been playing around with adding the method 'hasVoted' to the questionSchema but:
I'm not sure how to actually make the check happen.
I'm also not sure if there is a way for me to filter the votes array during the query (at MongoDB) instead of after node gets the results.
This is my attempt at the method which I know is wrong:
//Has the user already voted on this question?
questionSchema.methods.hasVoted = function (userid, cb) {
this.votes.filter(function(vote) {
if(userid == vote._id) {
return '1';
} else {
return '0';
}
});
};
I would recommend to make vote schema like so
var voteSchema = new Schema({
user: {type: Schema.ObjectId, ref: 'User', required: true},
vote : {type: Number, required: true}
})
var questionSchema = new Schema({
_id : {type: String, default: shortid.generate},
title : {type: String, required: true},
question : {type: String, required: true},
points : {type: Number, default: 0},
date : {type: Date, default: Date.now},
answers : [answerSchema],
votes : [{type: Schema.ObjectId, ref: 'Vote', required: false}]
});
Then just get your question and go through all the votes.
QuestionSchema.findById(question.id)
.populate('votes')
.exec(function (err, question) {
// go through all the votes here
}
or query if there is an question with your user id inside the votes
QuestionSchema.find()
.and([{_id:questionId,},{votes.user:userId}])
.populate('votes') //dunno if you really have to populate i think you don't have to
.exec(function (err, user) {
// check if(user)
}
or do it like described here findOne Subdocument in Mongoose
//EDIT
or if you don't change your schema
QuestionSchema.find({votes.user:userId})
.exec(function (err, user) {
// should return ALL questions where the user id is in the votes your want a specific question do it in a and like in the example above
}
and if you only want that one element from the array you have to make a projection like described here How to find document and single subdocument matching given criterias in MongoDB collection

Mongoose async call to another model makes validation impossible

I have two mongoose Schemas which look like this:
var FlowsSchema = new Schema({
name: {type: String, required: true},
description: {type: String},
active: {type: Boolean, default: false},
product: {type: Schema.ObjectId, ref: 'ClientProduct'},
type: {type: Schema.ObjectId, ref: 'ConversionType'},
});
this schema is then embedded in a parent schema that looks like this:
var ClientCampaignSchema = new Schema({
name: {type: String, required: true},
description: {type: String},
active: {type: Boolean, default: false},
activeFrom: {type: Date},
activeUntil: {type: Date},
client: {type: Schema.ObjectId, ref: 'Client', required: true},
flows: [FlowsSchema]
});
and
var ConversionTypeSchema = new Schema({
name: {type: Schema.Types.Mixed, required: true},
requiresProductAssociation: {type: Boolean, default: false}
});
As you can see my FlowsSchema holds a reference towards the ConversionType. What I want to do is only allow a product to be added to a flow, if the associated conversiontype's 'requiresProductAssociation' is equal to true.
Unfortunately, wether I use validators or middleware this would mean making a call to mongoose.model('ConversionType') which is automatically async and messes things up. What do?
p.s. if there would be a way to store a reference to that requiresProductAssociation boolean rather than the entire object that would be great because I wouldn't need to make the async call to that model anymore, but I don't know if that's possible.
The docs for SchemaType#validate describe how to perform asynchronous validation for cases like this. An asynchronous validator function receives two arguments, the second being a callback function you call to asynchronously report whether the value is valid.
That lets you implement this validation as:
FlowsSchema.path('type').validate(function(value, respond) {
mongoose.model('ConversionType').findById(value, function(err, doc) {
respond(doc && doc.requiresProductAssociation);
});
});

Resources