How to check if item exists in MonoDB array? - javascript

I have a MongoDB model:
const userSchema = new Schema = ({
name: { type: String },
company: [
companyId: {
type: String,
},
movies: [
{
genre: {
type: String,
enum: [
'horror',
'comedy',
'action',
'romance',
],
},
ratings: { type: String }
},
]
],
})
In my query, I have an endpoint that pushes a genre to the movies array but I want to check if there is an existing genre with the name already, if it exists, I want to show a message that says it already exists, otherwise, push the new items to the movies array
const result = await UserProfile.updateOne(
{
_id: id,
'company.companyId{ $eq: req.params.companyId},
'company.movies.$.genre': {
$eq: { genre: req.body.genre},
},
}
},
{
$push: {
'company.$.movies': {
...model,
},
},
},
{ new: true, runValidators: true }
).catch((err) => handleErrorThrow(err));
if (result.nModified === 0)
throw new CustomError(409, 'Movie exists already');
And if I want to remove the array based on another endpoint, I tried the same thing it doesn't work
const result = await UserProfile.updateOne(
{
_id: id
}
},
{
$pull: {
company: {
$elemMatch: {
companyId: req.params.companyId,
"movies.genre": {
$ne: req.body.genre
}
}
}
},
{ new: true, runValidators: true }
)
.catch((err) => handleErrorThrow(err));
if (result.nModified === 0)
throw new CustomError(409, 'Not exist');
It returned Not exist'

use $elemMatch for nested array condition, and $ne for genre should not exists before push into movies,
const result = await UserProfile.updateOne(
{
_id: id,
company: {
$elemMatch: {
companyId: req.params.companyId,
"movies.genre": {
$ne: req.body.genre
}
}
}
},
{
$push: {
"company.$.movies": model
}
},
{ runValidators: true }
)
.catch((err) => handleErrorThrow(err));
if (result.nModified === 0) {
throw new CustomError(409, 'Movie exists already');
}
And if I want to remove the array based on another endpoint
const result = await UserProfile.updateOne(
{
_id: id,
company: {
$elemMatch: {
companyId: req.params.companyId,
"movies.genre": req.body.genre
}
}
},
{
$pull: {
"company.$.movies": {
genre: req.body.genre
}
}
},
{ runValidators: true }
).catch((err) => handleErrorThrow(err));
if (result.nModified === 0) {
throw new CustomError(409, 'Not exist');
}

Related

How can I $push an item in two different fields, depending on the condition?

I'm trying to receive the user location and store it in the database. Also, the user can choose if he wants to save all his previous locations or not.
So I have created a boolean variable historicEnable: true/false.
So when the historicEnable is true, I want to push to historicLocation[] array in the UserSchema and if it is false, I want just to update currentLocation[] array in the UserSchema.
conntrollers/auth.js
exports.addLocation = asyncHandler(async (req, res, next) => {
const {phone, location, status, historicEnable} = req.body;
let theLocation;
if (historicEnable== true){
theLocation = await User.findOneAndUpdate(
{ phone },
{ $push:{ locationHistoric: location, statusHistoric: status }},
{ new: true }
)
} else if(historicEnable== false){
theLocation = await User.findOneAndUpdate(
{ phone },
{ location, status },
{ new: true }
)
}
res.status(200).json({
success: true,
msg: "A location as been created",
data: theLocation,
locationHistory: locationHistory
})
})
models/User.js
...
currentLocation: [
{
location: {
latitude: {type:Number},
longitude: {type:Number},
},
status: {
type: String
},
createdAt: {
type: Date,
default: Date.now,
}
}
],
historicLocation: [
{
locationHistoric: {
latitude: {type:Number},
longitude: {type:Number},
},
statusHistoric: {
type: String
},
createdAt: {
type: Date,
default: Date.now,
}
}
]
Also, not sure how to make the request body so the function works.
req.body
{
"phone": "+1234",
"historicEnable": true,
"loications": [
{
"location": {
"latitude": 25,
"longitude": 35
},
"status": "safe"
}
]
}
To sum up, if historicEnable is true, the data will be pushed in historicLocation, and if it false, to update the currentLocation.
How can I solve this?
You can use an update with an aggregation pipeline. If the historicEnable is known only on db level:
db.collection.update(
{phone: "+1234"},
[
{$addFields: {
location: [{location: {latitude: 25, longitude: 35}, status: "safe"}]
}
},
{
$set: {
historicLocation: {
$cond: [
{$eq: ["$historicEnable", true]},
{$concatArrays: ["$historicLocation", "$location"]},
"$historicLocation"
]
},
currentLocation: {
$cond: [
{$eq: ["$currentLocation", false]},
{$concatArrays: ["$currentLocation", "$location"]},
"$currentLocation"
]
}
}
},
{
$unset: "location"
}
])
See how it works on the playground example
If historicEnable is known from the input, you can do something like:
exports.addLocation = asyncHandler(async (req, res, next) => {
const phone = req.body.phone
const historicEnable= req.body.historicEnable
const locObj = req.body.location.locationHistoric[0];
locObj.createdAt = req.body.createdAt
const updateQuery = historicEnable ? { $push:{ locationHistoric: locObj}} : { $push:{ currentLocation: locObj}};
const theLocation = await User.findOneAndUpdate(
{ phone },
updateQuery,
{ new: true }
)
res.status(200).json({
success: true,
msg: "A location as been created",
data: theLocation,
locationHistory: locationHistory
})
})

How to add an object to an array of object, using addToSet, or push operators in mongodb

I have an array of reviews, I want to add a review using addToSet that will check if user is present in the array, then we do not want to add since one user can only review once.
My schema looks like this:
const sellerSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
unique: true,
},
reviews: [
{
by: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
unique: true,
},
title: {
type: String,
},
message: {
type: String,
},
rating: Number,
imagesUri: [{ String }],
timestamp: {
type: Date,
default: Date.now,
},
},
],
});
I might be doing the query wrong, but can't figure out how to add a review and check if current user has not reviewed before.
Here is the query where I add the review:
router.post("/review/:_id/", async (req, res) => {
try {
const stylist_id = mongoose.Types.ObjectId(req.params._id);
const review = {
by: req.user._id,
title: req.body.title,
message: req.body.message,
rating: parseFloat(req.body.rating),
};
if (req.body.imagesUri) {
//if there is images, we need to set it up
review.imagesUri = req.body.imagesUri;
}
await Seller.updateOne(
{ _id: seller_id },
{ $addToSet: { reviews: review } } //get the review that matches to the user_id
);
return res.status(200).send(true);
}catch(err){
res.status(502).send({
error: "Error creating a review.",
});
}
});
I'm thinking of checking for seller's id and also check that no review is by current user, but it is not working.
const userID = req.user._id;
await Seller.updateOne(
{ _id: seller_id, reviews: { $elemMatch: { by: { $ne: { userID } } } } },
{ $addToSet: { reviews: review } } //get the review that matches to the user_id
);
ANSWER:
I was able to solve the issue, in case other people have same issue. I did this:
await Seller.updateOne(
{
_id: seller_id,
"reviews.by": { $nin: [req.user.id] },
//knowing req.user._id is a mongoose.Types.ObjectId.
//You can also use [id1, id2, ...] to the array to check for other id's
},
{ $addToSet: { reviews: review } } //get the review that matches to the user_id
);
Here is the documentation for $nin operator: https://www.mongodb.com/docs/manual/reference/operator/query/nin/
You are pushing the review object inside an object.
Instead do this:
await Seller.updateOne(
{ _id: seller_id },
{ $addToSet: { reviews: review } }
);

JavaScript: Insert property into an object during loop

I'm looping an array of objects taken from MongoDB and attempting to insert a property into one of them, without success.
The array of objects would be:
[
{
_id: [String],
customerInformation: [ [Object] ],
purchasedBanners: [ [Object] ],
statusOfPurchase: 'new',
createdAt: 2021-02-24T15:04:42.074Z,
updatedAt: 2021-02-24T15:04:42.074Z,
__v: 0
}
...
]
I've tried:
return PurchasesModel.schemaForPurchases.find({
statusOfPurchase: args.statusOfPurchase
})
.limit(10)
.then(purchases => {
purchases.forEach(purchase => {
NotesModel.schemaForNotes.countDocuments({ purchaseId: purchase._id })
.then(numberOfNotes => {
Object.defineProperty(purchase, 'numberOfNotes', {
value: numberOfNotes
})
})
})
return purchases
})
But then I found that the forEach method is synchronous, so I tried:
return PurchasesModel.schemaForPurchases.find({
statusOfPurchase: args.statusOfPurchase
})
.limit(10)
.then(purchases => {
for (let i = 0; i < purchases.length; i++) {
let numberOfNotes = 0
numberOfNotes = NotesModel.schemaForNotes.countDocuments({ purchaseId: purchases[i]._id })
.then(numberOfNotes => {
return numberOfNotes
})
Object.defineProperty(purchases[i], 'numberOfNotes', {
value: numberOfNotes.then(numberOfNotes => {
return numberOfNotes
})
})
}
return purchases
})
In each case (including several other approaches), the objects aren't appended.
I'm new to MongoDB, so I assume I'm either doing something wrong, or perhaps the objects are somehow protected?
Thoughts welcome.
In the end, there wasn't a shortcut! Or at least I'm not aware of it.
const GET_ALL_PURCHASES_QUERY = (statusOfPurchase) => {
return gql`
query {
getAllPurchases(statusOfPurchase: "${statusOfPurchase}") {
id
customerInformation {
customerName
customerEmailAddress
}
purchasedBanners {
nameOfBanner
costOfBanner
numberOfBannersToPrint
nameOfChosenComponent
targetPDF
previewImage
dataToExport
}
numberOfNotes {
count
}
createdAt
updatedAt
}
}
... and then:
const NotesCountForPurchaseType = new GraphQLObjectType({
name: 'NotesCountForPurchase',
fields: () => ({
count: {
type: GraphQLInt
}
})
})
const PurchaseType = new GraphQLObjectType({
name: 'Purchase',
fields: () => ({
id: {
type: GraphQLID
},
customerInformation: {
type: GraphQLList(PurchaseCustomerInformationType)
},
purchasedBanners: {
type: GraphQLList(PurchaseBannerType)
},
statusOfPurchase: {
type: GraphQLString
},
createdAt: {
type: GraphQLDateTime
},
updatedAt: {
type: GraphQLDateTime
},
numberOfNotes: {
type: NotesCountForPurchaseType,
resolve(parent, args) {
return NotesModel.schemaForNotes.countDocuments({
purchaseId: parent.id
})
.then(numberOfNotes => {
console.log('Schema:numberOfNotes()', numberOfNotes)
return { count: numberOfNotes }
})
}
}
})
})
Extra work, but working.

Implement feed with retweets in MongoDB

I want to implement retweet feature in my app. I use Mongoose and have User and Message models, and I store retweets as array of objects of type {userId, createdAt} where createdAt is time when retweet occurred. Message model has it's own createdAt field.
I need to create feed of original and retweeted messages merged together based on createdAt fields. I am stuck with merging, whether to do it in a single query or separate and do the merge in JavaScript. Can I do it all in Mongoose with a single query? If not how to find merge insertion points and index of the last message?
So far I just have fetching of original messages.
My Message model:
const messageSchema = new mongoose.Schema(
{
fileId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'File',
required: true,
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
likesIds: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
reposts: [
{
reposterId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
createdAt: { type: Date, default: Date.now },
},
],
},
{
timestamps: true,
},
);
Edit: Now I have this but pagination is broken. I am trying to use newCreatedAt field for cursor, that doesn't seem to work. It returns empty array in second call when newCreatedAt is passed from the frontend.
messages: async (
parent,
{ cursor, limit = 100, username },
{ models },
) => {
const user = username
? await models.User.findOne({
username,
})
: null;
const options = {
...(cursor && {
newCreatedAt: {
$lt: new Date(fromCursorHash(cursor)),
},
}),
...(username && {
userId: mongoose.Types.ObjectId(user.id),
}),
};
console.log(options);
const aMessages = await models.Message.aggregate([
{
$addFields: {
newReposts: {
$concatArrays: [
[{ createdAt: '$createdAt', original: true }],
'$reposts',
],
},
},
},
{
$unwind: '$newReposts',
},
{
$addFields: {
newCreatedAt: '$newReposts.createdAt',
original: '$newReposts.original',
},
},
{ $match: options },
{
$sort: {
newCreatedAt: -1,
},
},
{
$limit: limit + 1,
},
]);
const messages = aMessages.map(m => {
m.id = m._id.toString();
return m;
});
//console.log(messages);
const hasNextPage = messages.length > limit;
const edges = hasNextPage ? messages.slice(0, -1) : messages;
return {
edges,
pageInfo: {
hasNextPage,
endCursor: toCursorHash(
edges[edges.length - 1].newCreatedAt.toString(),
),
},
};
},
Here are the queries. The working one:
Mongoose: messages.aggregate([{
'$match': {
createdAt: {
'$lt': 2020 - 02 - 02 T19: 48: 54.000 Z
}
}
}, {
'$sort': {
createdAt: -1
}
}, {
'$limit': 3
}], {})
And the non working one:
Mongoose: messages.aggregate([{
'$match': {
newCreatedAt: {
'$lt': 2020 - 02 - 02 T19: 51: 39.000 Z
}
}
}, {
'$addFields': {
newReposts: {
'$concatArrays': [
[{
createdAt: '$createdAt',
original: true
}], '$reposts'
]
}
}
}, {
'$unwind': '$newReposts'
}, {
'$addFields': {
newCreatedAt: '$newReposts.createdAt',
original: '$newReposts.original'
}
}, {
'$sort': {
newCreatedAt: -1
}
}, {
'$limit': 3
}], {})
This can be done in one query, although its a little hack-ish:
db.collection.aggregate([
{
$addFields: {
reposts: {
$concatArrays: [[{createdAt: "$createdAt", original: true}],"$reports"]
}
}
},
{
$unwind: "$reposts"
},
{
$addFields: {
createdAt: "$reposts.createdAt",
original: "$reposts.original"
}
},
{
$sort: {
createdAt: -1
}
}
]);
You can add any other logic you want to the query using the original field, documents with original: true are the original posts while the others are retweets.

mongodb after updating document, returns old values

router.delete('/deletepost', (req, res) => {
// console.log(req.query.postid)
if (req.query.category === 'forsale') {
ForSalePosts.findById(req.query.postid)
// .then(post => console.log(post))
.deleteOne()
.catch(err => console.log(err))
AllPosts.updateOne({ user: req.query.userid },
{ $pull: { posts: { postid: req.query.postid } } })
.catch(err => console.log(err))
AllPosts.aggregate(
[
{ $match: { user: ObjectId(req.query.userid) } },
{ $unwind: '$posts' },
{ $sort: { 'posts.date': -1 } }
]
)
.then(posts => {
// console.log(posts)
res.json(posts)
})
.catch(err => res.status(404).json({ nopostfound: 'There is no posts' }))
}
})
this is my route. i am trying to delete an item in my document. the item is being deleted however it returns old values. for example :
Allposts has an array with posts:[postid:{type:String}, ...]
I am trying to delete a specific postid by using $pull,
postid is being deleted however when I aggregate the same model, .then(posts=> console.log(posts)) returns old values on first call, doesnt update the component.
EDIT: just realized sometimes it returns the right values but sometimes it returns the old values as well. does anyone know why and what can i do to solve it ?
const mongoose = require('mongoose')
const Schema = mongoose.Schema;
const AllPostsSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'users'
},
posts: [{
postid: {
type: String
},
title: {
type: String
},
category: {
type: String
},
subcategory: {
type: String
}, category: {
type: String
},
description: {
type: String
},
name: {
type: String
},
price: {
type: Number
},
email: {
type: String
},
phonenumber: {
type: Number
},
language: {
type: String
},
make: {
type: String
},
model: {
type: Number
},
odometer: {
type: Number
},
condition: {
type: String
},
state: {
type: String
},
town: {
type: String
},
city: {
type: String
},
links: [{ type: String }],
date: {
type: Date,
default: Date.now
}
}]
})
module.exports = AllPosts = mongoose.model('allposts', AllPostsSchema)
REACT FUNCTION CALL :
deletePost = (category, postid) => {
const postinfo = {
category: category.toLowerCase(),
postid: postid,
userid: this.props.auth.user.id
}
this.props.deletePost(postinfo)
}
You need to add options parameter to delete like:
AllPosts.updateOne({ user: req.query.userid },
{
$pull: { posts: { postid: req.query.postid } }
},
{ new: true }
);
This will return the new object after performing the operation. Hope this works for you.
All the mongo queries return partial promise. You have to use .then in order to resolve each promises.
Here you are running all the queries in series without using .then or async-await. So whenever you $pull from AllPosts after that immediately you call the AllPosts aggregate query which sometimes get executed and sometimes it doesn't.
So in order to make it run one by one you have to use either .then or async-await.
router.delete("/deletepost", (req, res) => {
if (req.query.category === "forsale") {
ForSalePosts.findById(req.query.postid)
.deleteOne()
.then(() => {
AllPosts.updateOne(
{ "user": req.query.userid },
{ "$pull": { "posts": { "postid": req.query.postid } } }
)
.then(() => {
AllPosts.aggregate([
{ "$match": { "user": ObjectId(req.query.userid) } },
{ "$unwind": "$posts" },
{ "$sort": { "posts.date": -1 } }
]).then(posts => {
// console.log(posts)
res.json(posts);
});
})
.catch(err =>
res.status(404).json({ "nopostfound": "There is no posts" })
);
});
}
})

Categories

Resources