Comparing JSON array with Mongo array document - mongodb

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.

Related

Documents inserted without schema not being found with schema

I have two new collections in MongoDB of data that I pulled from an old Firestore database that I'm moving to mongo. Since the total number between these two collections is roughly 20,000, I opted to paste the raw JSON into the insert document section in mongo, which worked like a charm and I didn't have to write a new insert route to do the same.
I then created a schema in Mongoose that matched the inserted documents, and tried to use the schema to pull back some data, and its always returning nothing.
An example of a ticket inserted via JSON:
{
"title": "How to add and manage users for your company in QuickBooks Online",
"priority": "1",
"type": "Video",
"course": "G205",
"transcriptId": "07dom27Zz98jakvB1oh5",
"status": "In Review",
"tags": "",
"url": "",
"revisionNumber": 0,
"directoryId": 19,
"checkedOut": false
},
And my schema I made to match. The collection name in mongo is also called oldTickets, the plural of my schema name here:
const mongoose = require('mongoose');
var Schema = mongoose.Schema
const schema = new Schema({
course: { type: String },
title: { type: String },
priority: { type: String },
type: { type: String },
course: { type: String },
transcriptId: { type: String },
status: { type: String },
tags: { type: String },
url: { type: String },
revisionNumber: { type: Number },
directoryId: { type: Number },
checkedOut: { type: Boolean },
});
module.exports = mongoose.model('oldTicket', schema);
And finally my model import and fetch call:
const OldTicket = require('./models/model_old_ticket');
/***************************************************************************
* Get Old Tickets - Returns all old tickets, 10 at a time
****************************************************************************/
app.get('/getOldTickets/:offset', (req, res) => {
checkConnection();
OldTicket.find().skip(parseInt(req.params.offset)).limit(10).exec((err, data) => {
if (err){ res.status(500).send({err: err}); }
//If we got data, count the tickets & return the tickets & count
if (data) {
OldTicket.find().countDocuments().then(count => {
return res.status(200).send({
tickets: data,
count: count
})
})
}
});
});
Why isn't this finding anything? Both the count and the tickets are 0. I've run into this issue before when manually creating a collection without a schema, and in those instances I would simply delete the collection, write a route to create a document, and then things would work fine. But with the large data size of these two collections, I'd rather not do that since everything should be working as is.
Edit: Example of document in Mongo
And the name of the collection I'm currently viewing:
And I just now realized that for some reason there are now two collection names, oldTickets, which has data, and oldtickets, which is empty. I'm assuming my query is searching through the empty one? How can I get it to go to the one that actually has data?
can you attach the screenshot of your data with the collection? might be it's different.in mongoose, every collection name is complete with 's'. please verify your collection is created manually by you then it has to same as mongoose schema and also completed with 's'.
example:
const mongoose = require("mongoose");
const schema = new mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
index: true
},
filmId: {
type: mongoose.Schema.Types.ObjectId,
index: true
},
filmType: {
type: String,
index: true
},
birthday: {
type: Date
},
age: {
type: Number
},
terms: {
type: Boolean
}
},
{
versionKey: false,
timestamps: true,
}
);
schema.index({ filmId: 1, user: 1 })
module.exports = mongoose.model("UserAgeVerification", schema);
see my database

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)

Validate array of strings in mongoose

I wanted to validate each value from the request which is array of strings. Something like
emails: [ 'johndoe#gmail.com', 'jandoe#gmail.com' ]
Here is my schema
const UserSchema = mongoose.Schema({
name: {
type: String,
index: true,
required: true,
},
emails: [String],
});
In my validation I wanted to make sure that each email is not already exists in the database. I've tried the following
body("emails").custom((value, { req }) => {
return User.findOne({
emails: { $all: value },
_id: { $ne: req.params.id },
}).then((exists) => {
if (exists) {
return Promise.reject("Email already exists!");
}
});
});
But the problem is if I tried to post multiple emails in array the validation fails and the data will be inserted to db. How can I check if one of the emails already exists and reject the request?
In the docs of $in, it mentioned that:
If the field holds an array, then the $in operator selects the documents whose field holds an array that contains at least one element that matches a value in the specified array...
So you can solve it by:
User.findOne({
emails: { $in: value },
_id: { $ne: req.params.id },
})...

GraphQL Mutation Updating Users Followers with Mongoose/MongodDB - $set is empty error

I have this mutation set up:
followUser: {
type: UserType,
args: {
_id: { type: GraphQLString },
firebaseUid: { type: GraphQLString },
following: { type: new GraphQLList(GraphQLString)},
},
resolve(parentValue, { firebaseUid, _id, following}) {
const update = {
$set: { "following": [firebaseUid] },
$push: { "following": { firebaseUid } }
}
return UserSchema.findOneAndUpdate(
{ _id },
update,
{new: true, upsert: true}
)
}
},
I'm trying to add new followers into my graphql user's collection. My user model:
const UserSchema = new Schema(
{
firebaseUid: String,
following: [{ type: Schema.Types.ObjectId, ref: 'User' }],
followers: [{ type: Schema.Types.ObjectId, ref: 'User' }],
},
{ timestamps: true }
);
module.exports = mongoose.model("User", UserSchema);
So at first, the user doesn't have any followers, so it won't have that field yet. When user adds someone to their friends list, thats when the field will appear in mongodb. Right now I'm getting this error:
"message": "'$set' is empty. You must specify a field like so: {$set: {<field>: ...}}",
I'm not sure if I'm doing the $set correctly.
The UserType
const UserType = new GraphQLObjectType({
name: "User",
fields: () => ({
_id: { type: GraphQLString },
firebaseUid: { type: GraphQLString },
following: { type: new GraphQLList(GraphQLString) },
followers: { type: new GraphQLList(GraphQLString) },
...
})
});
edit:
current mongodb data collection:
_id: ObjectId("5e5c24111c9d4400006d0001")
name: "Mr. Smith"
username: "mrsmith"
after running the update
_id: ObjectId("5e5c24111c9d4400006d0001")
name: "Mr. Smith"
username: "mrsmith"
following: ["fdsaduybfeaf323dfa"] // <-- this gets added
Currently mongooses validator is rejecting the update. To fix this you need the following:
You only need to $push since it will automatically create an array if the property does not exist
You should remove the extra { } around the firebaseUid in the $push because otherwise the following array will contain objects with a firebaseUid property instead of directly containing the Uid (or would if the schema validator allowed it)
Mongo ObjectIds can only be converted from strings when they are 12-byte hexadecimal, and firebaseUid is not, so the schema should be typed to String instead of ObjectId as the validator will reject the field for update otherwise.

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);