Mongoose.js: is it possible to populate array items by condition? - mongodb

I have this schema:
var PostSchema = new Schema({
by: { type: mongoose.Schema.Types.ObjectId, ref:'user' },
body: String,
likes: [{ by: { type: mongoose.Schema.Types.ObjectId, ref:'user' }, isAnonymous: { type: Boolean, default: false}}],
});
This schema supports anonymous likes.
my question: is there a way to populate only the items in the 'likes' array where isAnonymous is equals to false?

Yes, There is
opts={
path: 'likes',
match: {
isAnonymous:false
},
select: '<fields to be fetched>'
};
Post
.find({})
.populate(opts)
docs
help

Related

Mongoose populate depending on conditions

My service uses MongoDB and Mongoose. I have two DBs: Users and Posts. In Posts schema I have parameters:
"author", that contains userId from Users DB
"anonymous", a boolean-parameter that shows if the post is anonymous or not.
I can't solve the problem: when I request data from Posts DB I want to populate author in the "author" parameter only for non-anonymous posts, for anonymous ones I'd like to return null or not to return this parameter at all.
I've tried to use "match", but it doesn't work.
How can I solve this problem?
Thank you.
Code example:
const postSchema = mongoose.Schema(
{
author: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User',
},
anonymous: {
type: Boolean,
required: true,
default: false,
},
content: {
type: String,
required: true,
},
date: {
type: Date,
required: true,
default: Date.now,
},
},
{
timestamps: true,
}
);
For population I use pre:
postSchema.pre(/^find/, function (next) {
this.populate({
path: 'author',
select: '_id login',
});
next();
});

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.

Collection referencing multiple collections

I want to have a collection with multiple fields referencing multiple collections, something like it:
var comboSchema = new Schema({
oneId: { type: Schema.Types.ObjectId, ref: "One" },
twoId: { type: Schema.Types.ObjectId, ref: "Two" },
threeId: { type: Schema.Types.ObjectId, ref: "Three" },
components: {
id: {type: Schema.Types.ObjectId, ref: "Component"},
amount: {type: Number}
}
}
I know I can use $lookup and aggregate to get data, but it looks like it works only on a single field in a collection?
Any help? Thank you! :-)
This is a model sample using the ref, the ref key in the object will take the name of the model in which you are referencing
const mongoose = require('mongoose');
const postSchema = mongoose.Schema({
text: {
type: String,
required: 1
},
mediatype: {
type: String,
required: 1
},
media: {
type: String,
required: true
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'user'
},
likes: {
type: [{
userid: {
type: mongoose.Schema.Types.ObjectId,
ref: 'user'
}
}]
},
comments: {
type: [{
userid: {
type: mongoose.Schema.Types.ObjectId,
ref: 'user'
},
comment: String
}]
},
}, {
timestamps: true
})
const Post = mongoose.model('post', postSchema)
module.exports = Post
so you can then populate it like Post.find().populate('user')

MongoDB schema: Like a Comment OR a Post

I would like to setup a "like" system in my app. User should be able to like either Posts or Comments (Comments of a Post of course). How should I design this?
Users
const userSchema = new Schema({
id: { type: String, required: true },
username: { type: String, required: true },
password: { type: String, required: true },
});
Posts
const postSchema = new Schema({
content: { type: String, required: true },
authorId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }
});
Comments
const commentSchema = new Schema({
content: { type: String, required: true },
authorId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
postId: { type: mongoose.Schema.Types.ObjectId, ref: "Post", required: true },
});
Likes
const likeSchema = new Schema({
content: { type: String, required: false },
authorId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
postId: { type: mongoose.Schema.Types.ObjectId, ref: "Post", required: function() { return this.commentId? false : true } },
commentId: { type: mongoose.Schema.Types.ObjectId, ref: "Comment", required: function() { return this.postId? false : true } }
});
I'm coming from relational databases, and maybe my design is completely wrong for nosql. My main interrogation is about Likes, I have no idea how to accept likes on Posts OR Comments.
I would prefer a separate collection:
User:
id:
...
Post:
id:
userId:
...
Comment:
id:
userId:
postId:
Like:
id:
userId:
postId:
commentId:
The second one storing an array will lead you cyclic dependencies in the backend. Especially, when you use NodeJS and strict to flow.
MongoDB is powerful at storing documents. Documents hold the relations.
I would model it in the way your data is being accessed. I do recommend playing around with the powerful aggregation framework and array operators to experience the possibilities. What I would explore is the following
User:
id:
name:
picture:
...
Posts:
id:
authorid:
content:
total_views:
tags: array of String
likes: array of Likes {[
liked_by: user_id
],...}
comments: array of Comments {[
author_id: ...
comment: ...
reactions: array of Comments {[],...}
likes: array of Likes {[
liked_by: user_id
],...}
],...}
Will this model scale? Documents can hold 16MB of data. 16MB in textual format is HUGE.
PS please think again on storing username/password in the database. This is a whole other discussion. Look into the topics of authentication, authorisation, OAuth, hashing/salting etc.
post={
...keys,
likes:[likeSchema],
comments:[CommentSchema]
}
this is i prefer, even if you want to store recursive comments just use
commentschema={
id:unique commet id
text:...
user_id:who wrote this comment
parent_id: to which this comment belongs to!
depth: comment depth as your wish (mostly 2)
}
parent id will be null for a comment posted directly on post
parent id will be comment_id of the comment to which this comment posted for. if its a recursive comment.
hope you get it.
Since, the question is about schema for like a comment or post. I'll focus on likes.
Build a schema like this. Here targetId will be postId or commentId.
const likeSchema = new Schema({
content: { type: String, required: false },
authorId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
targetId: { type: mongoose.Schema.Types.ObjectId, ref: "Post", required: function() { return this.commentId? false : true } }
});
Some points you need to consider:
Store likes of posts in post collection
Store likes of comments in comments collection
You need to build a mechanism to calculate likes and store in that collection

Ordering two reference arrays together

Suppose I have the following schemas:
var QuizSchema = new mongoose.Schema({
name: { type: String, required: true },
questions: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Question' }],
questionGroups: [{ type: mongoose.Schema.Types.ObjectId, ref: 'QuestionGroup' }]
});
var QuestionSchema = new mongoose.Schema({
number: { type: String, required: true }, // e.g. 1, a, i, anything
question: { type: String, required: true },
type: { type: String, enum: ['multiple choice', 'multiple select', 'short answer'] },
choices: [String],
answers: [String]
});
var QuestionGroupSchema = new mongoose.Schema({
number: { type: String, required: true }, // e.g. 1, a, i, anything
prompt: { type: String },
questions: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Question' }]
});
I am trying to design a way that will allow me to order questions and question groups together.
I was thinking maybe of adding a new field order
var QuizSchema = new mongoose.Schema({
// ...
order: [
{
type: { type: String, enum: ['Question', 'QuestionGroup'] },
id: mongoose.Schema.Types.ObjectId // reference
}
]
});
such that in the database, the field would contain something such as
[
{ type: 'Question', id: ObjectId('57867a34567g67790') },
{ type: 'Question', id: ObjectId('57867a34567g67765') },
{ type: 'QuestionGroup', id: ObjectId('69864b64765y45645') },
{ type: 'Question', id: ObjectId('57867a34567g67770') },
{ type: 'QuestionGroup', id: ObjectId('69864b64767y45647') }
]
This may mean that I would need to "populate" the ordered list of questions and question groups as
quiz.populate('questions questionGroups').exec(function (err, quiz) {
// sort questions and groups by the order
quiz.order = quiz.order.map(function (o) {
if (o.type === 'QuestionGroup') {
return quiz.questionGroups.id(o.id);
}
return quiz.questions.id(o.id);
});
});
So my question: is there a better way to design this?
Virtuals can come in handy here; without persisting order field in db and doing calculations on client each time:
var QuizSchema = new mongoose.Schema({
name: { type: String, required: true },
questions: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Question' }],
questionGroups: [{ type: mongoose.Schema.Types.ObjectId, ref: 'QuestionGroup' }]
},
{
toObject: {
virtuals: true
},
toJSON: {
virtuals: true
}
}
);
QuizSchema
.virtual('order')
.get(function() {
return this.questions.concat(this.questionGroups); //questions followed by questionGroups
});
Sort on createdAt is of course optional, but for that you need to have this field in Question and QuestionGroup:
Quiz.find({}, function (err, quiz) {
//...
})
.populate({path : 'questions', options: {sort: { 'createdAt': 1 }}})
.populate({path : 'questionGroups', options: {sort: { 'createdAt': 1 }}});