What's a good way to look up list of enumerated values? - mongodb

I have a collection of case with a field named status (integer) whose valid values are 0, 1, 2, 4 and 8, representing "New", "WIP", "Solved", "Canceled" and "Closed" respectively.
So, in mongoose syntax, it might be like:
const caseSchema = new Schema({
createdOn: Date,
subittedBy: String,
status: Number,
...
});
const statusSchema = new Schema({
value: Number,
description: String
});
Is this a good way to organize the data? How do I make a query to retrieve cases with the status field properly filled with the description?

It is one way to do it sure. You could do the query by using $lookup. It would look something like this:
db.getCollection('<YourCasesColName>').aggregate([
{ $match : { 'status' : 1 } }, // or { $in: [1,2,3] },
{
$lookup: {
from: '<YourStatusColName>',
localField: 'status',
foreignField: 'value',
as: 'statusDoc',
}
}
])
Another way is to add a reference to the actual status via ObjectId so that instead of numbers in the cases you would be storing references to the actual Status objects and in this way have a better referential integrity. However you would still need to do similar query to get both in one shot. So here is what I am talking about:
const caseSchema = new Schema({
createdOn: Date,
subittedBy: String,
status: { type: mongoose.Schema.Types.ObjectId, ref: 'Status' },
// ^ now your status hows reference to exactly the type of status it has
});
const statusSchema = new Schema({
value: Number,
description: String
});
So the actual data would look like this:
// Statuses
[{
_id: <StatusMongoObjectID_1>,
value: 1,
description: 'New'
},{
_id: <StatusMongoObjectID_2>,
value: 2,
description: 'New'
}]
// Cases
[{
_id: <MongoObjectID>,
createdOn: '<SomeISODate>',
subittedBy: '<SomeString>',
status: <StatusMongoObjectID_1>
},
{
_id: <MongoObjectID>,
createdOn: '<SomeISODate>',
subittedBy: '<SomeString>',
status: <StatusMongoObjectID_2>
}]

Related

Getting total number of likes that user received by going through all his/her posts MongoDB

I'm currently using MERN stack to create a simple SNS application.
However I got stuck trying to come up with a query that could go through all of the post that user posted and get the sum of likes.
Currently I've created 3 Schemas. User, Post, Reply.
User
const userSchema = new Schema({
facebookId: {
required: true,
type: String,
},
username: {
required: true,
type: String,
},
joined: Date
})
POST
const postSchema = new Schema({
title: String,
body: String,
author: { type: Schema.Types.ObjectId, ref: "User" },
datePosted: Date,
reply: [{ type: Schema.Types.ObjectId, ref: 'Reply'}],
tag: [ {type: String} ]
});
REPLY
const replySchema = new Schema({
title: String,
body: String,
author: { type: Schema.Types.ObjectId, ref: "User" },
datePosted: Date,
post: [{ type: Schema.Types.ObjectId, ref: 'Post'}],
likes: [{ type: Schema.Types.ObjectId, ref: "User" }] // storing likes as array
});
As you can see I have added likes field in Reply Schema as an array that takes in User ObjectId.
Say some user has posted 4 replies and each of them received 1 ,3, 4, 5 likes respectively. In the leaderboard section I want to display user info with the total number of counts that they received from all of their replies which is 1+3+4+5 = 13 likes.
Can this be achieved without adding additional fields or is there a better way of structuring the schema.
If this field is going to be shown publicly then I personally recommend that instead of calculating on the fly you pre-calculate it and save it on the user as aggregating data is expensive and should not be a part of your app's logic, especially if this needs to be calculated for each user for the leaderboard feature.
With this said here is how you can calculate it with the aggregation framework
db.replies.aggregate([
{
$match: {
author: userid
}
},
{
$group: {
_id: null,
likes: {$sum: {$size: '$likes'}}
}
}
]);
As I said I recommend you do this offline, run this once for each user and save a likeCount field on the user, you can then update your other routes where a like is created to update the user like count using $inc.
// .. new like created ...
db.users.updateOne({_id: liked_reply.author}, {$inc: {likeCount: 1}})
And now finding the leaderboard is super easy:
const leaders = await db.users.find({}).sort({likeCount: -1}).limit(10) //top 10?
you can use the models aggregate function to do that:
const userid = 1234
post.aggregate([
{ $match: { _id: userid } },
{ $lookup: {
from: 'Reply',
localField: 'reply',
foreignField: '_id',
as: 'replies'
} },
{ $group: {
_id: false,
sumoflikes: { $sum: '$replies.likes' }
} }
])
the structure works as follows:
get all the posts from a user with 'userid'
join the table with 'Reply'
sum all of the reply.likes
(it could be that you need to throw in a $unwind: '$replies between 2 and 3 there, i am not 100% certain)

Comparing JSON array with Mongo array document

Currently I have a mongoose model 'Event' that stores a list of UUIDs as participants that reference another model 'User'
.
participants: [{
_id: false,
id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
tickets: {
type: Number,
min: 0,
},
}],
winners: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}],
.
.
Now I receive a request with the following JSON data to update my winners
{
"winners": [
"5f61132da98bac2a98487d79",
"5f611378a98bac2a98487d7a",
"5f611378a98bac2a98487d77"
]}
Is there a way to compare this array with participant field of that model and only allow entry of user ids that are participants? For example
const event = await Event.findOne({_id: _id}, 'participants.id -_id');
console.log(event.participants);
Output:
[
{ id: 5f61132da98bac2a98487d79 },
{ id: 5f611378a98bac2a98487d7a },
{ id: 5f6113b1a98bac2a98487d7b }
]
console.log(req.body.rewards);
[
'5f61132da98bac2a98487d79',
'5f611378a98bac2a98487d7a',
'5f611378a98bac2a98487d77'
]
Clearly the last UUID is not matching (is not a participant) so should be discarded and other two remaining should be updated in winners field. JSON object can be made flexible if needed.
Using aggregation framework does not really help me on this problem, and it might be very costly to project specific elements for a single document only. A work around solution would be using arrays.
const event = await Event.findOne({
_id: _eventId,
rewards: { $elemMatch: { _id: _rewardId }}
}, 'participants.id -_id');
const allParticipants = []
(event.participants).forEach((item) => {
allParticipants.push(item.id.toString());
});
const validIds = (req.body.winners).filter(item => allParticipants.includes(item));
This will check for arrays using includes and filter and return matching array item from the request and participants in the event.

MongoDB Query from multiple models / schemas and return in one field

I am using Nodejs and MongoDB, mongoose and expressjs, creating a Blog API having users, articles, likes & comments schema. Below are schemas that I use.
const UsersSchema = new mongoose.Schema({
username: { type: String },
email: { type: String },
date_created: { type: Date },
last_modified: { type: Date }
});
const ArticleSchema = new mongoose.Schema({
id: { type: String, required: true },
text: { type: String, required: true },
posted_by: { type: Schema.Types.ObjectId, ref: 'User', required: true },
images: [{ type: String }],
date_created: { type: Date },
last_modified: { type: Date }
});
const CommentSchema = new mongoose.Schema({
id: { type: String, required: true },
commented_by: { type: Schema.Types.ObjectId, ref: 'User', required: true },
article: { type: Schema.Types.ObjectId, ref: 'Article' },
text: { type: String, required: true },
date_created: { type: Date },
last_modified: { type: Date }
});
What I actually need is when I * get collection of articles * I also want to get the number of comments together for each articles. How do I query mongo?
Since you need to query more than one collection, you can use MongoDB's aggregation.
Here: https://docs.mongodb.com/manual/aggregation/
Example:
Article
.aggregate(
{
$lookup: {
from: '<your comments collection name',
localField: '_id',
foreignField: 'article',
as: 'comments'
}
},
{
$project: {
comments: '$comments.commented_by',
text: 1,
posted_by: 1,
images: 1,
date_created: 1,
last_modified: 1
}
},
{
$project: {
hasCommented: {
$cond: {
if: { $in: [ '$comments', '<user object id>' ] },
then: true,
else: false
}
},
commentsCount: { $size: '$comments' },
text: 1,
posted_by: 1,
images: 1,
date_created: 1,
last_modified: 1
}
}
)
The aggregation got a little big but let me try to explain:
First we need to filter the comments after the $lookup. So we $unwind them, making each article contain just one comment object, so we can filter using $match(that's the filter stage, it works just as the <Model>.find(). After filtering the desired's user comments, we $group everything again, $sum: 1 for each comment, using as the grouper _id, the article's _id. And we get the $first result for $text, $images and etc. Later, we $project everything, but now we add hasCommented with a $cond, simply doing: if the $comments is greater than 0(the user has commented, so this will be true, else, false.
MongoDB's Aggregation framework it's awesome and you can do almost whatever you want with your data using it. But be aware that somethings may cost more than others, always read the reference.

How to Aggregate data from a set within a document

I have a document that's modeled like this:
{
_id: '1',
timeEntries: [
{ _id: '1',
hours: '1'
},
{ _id: '2',
hours: '2'
},
],
totalHours: 3
}
Right now, every time I add a timeEntry to the set of timeEntries in my document, I also increment the totalHours property by the hours in the added timeEntry.
Instead of incrementing every time I $addToSet, I want to be able to call a method on the model of my document (virtual field?) to get the total hours from the document's timeEntries.
How would I go about doing this?
Update
Additionally, my timeEntries are actually stored as an array of reference.
Example:
[ 5bcf5e53e452b800134787dd, 5bcf5f42e452b800134787de ]
And I need a way to populate the hours property inside of the virtual field
Schema:
const companyHourLogSchema = new Schema({
dateOpened: {
type: Date,
default: Date.now,
},
dateClosed: {
type: Date,
},
_id: {
type: mongoose.Schema.ObjectId,
auto: true,
},
company: {
type: mongoose.Schema.ObjectId,
ref: 'Company',
},
timeEntries: [{
type: mongoose.Schema.ObjectId,
ref: 'TimeEntry',
}],
title: {
type: String,
default: 'Current',
},
});
update
because the time entries are saved as references you need to use mongoose populate to get the entries first
Model.find(/*your condition here*/).populate('timeEntries');
to get the timeEntries populated with the log.
you can use virtuals as follows:
schema.virtual('totalHours').get(function () {
return this.timeEntries.reduce((agg,tiE) => agg + tiE.hours ,0);
});
then you can use it as follows:
console.log(modelInstance.totalHours);

Mongoose, proper way to update documents with deep nested schemas

I would like to know what would be the most efficient way to update a document that also has a nested schema in it. Normally I would just use Model.findByIdAndUpdate(id, updatedValues) ..., however if I try to do that with a document that has a deep nested schema I get this error: CastError: Cast to Embedded failed for value "{ _id: 5b1936aab50e727c83687797, en_US: 'Meat', lt_LT: 'Mesa' }" at path "title".
My Schemas look something like:
const titleSchema = new Schema({
en_US: {
type: String,
required: true
},
lt_LT: {
type: String,
default: null
}
})
const categorySchema = new Schema({
title: titleSchema
});
const ingredientSchema = new Schema({
title: {
type: titleSchema,
required: true
},
category: categorySchema,
calories: {
type: Number,
min: 0,
default: 0
},
vitamins: [String]
})
And I try to update like so:
{
title: {
en_US: 'Pork',
lt_LT: 'Kiauliena'
},
category: {
_id: '5b193a230af63a7e80b6acd8',
title: {
_id: '5b193a230af63a7e80b6acd7'
en_US: 'Meat',
lt_LT: 'Mesa'
}
}
}
Note, I get the new category object from a separate collection using just the category _id, but the final update object that goes into the findByIdAndUpdate() function looks like the one above.
The only workout I found is to remove the category from the updated values, update the document via findByIdAndUpdate(), get the updated document, assign the new category to it and save it.
It works just fine and all, but that requires two calls to the database, is it possible to do it in just one?
Try updating your schema like this:
const titleSchema = new Schema({
en_US: {
type: String,
required: true
},
lt_LT: {
type: String,
default: null
}
});
const ingredientSchema = new Schema({
title: {
type: titleSchema,
required: true
},
category: {
title: titleSchema
},
calories: {
type: Number,
min: 0,
default: 0
},
vitamins: [String]
});