I am at my wits end with something that is seemingly straightforward:
I need to be able to push new gifts into the Events Array under the specific user. Because each event will have numerous gifts added, I want to keep them all under the user, as they are the one creating the event, and the gifts will live inside of their event where they belong.
The PROBLEM is: when I use the mongoose method 'findByIdAndUpdate', I can only find the main user, and from there, push an event to the events array. What I NEED to be able to do: push gifts to a specific event under that user. I am using mongoose Subdocuments. See my schema below and how I have a subdocument schema (EventSchema) inside of the main user schema, and a subdocument (gift) schema inside the event schema.
SCHEMA:
const Schema = mongoose.Schema;
let giftArr = new Schema({
giftname: String,
giftlink: String,
claimed: Boolean,
claimee: String
})
let eventSchema = new Schema({
eventname: String,
eventowner: String,
date: {
type: Date,
default: Date.now
},
attendees: [
{
attendeename: String
}
],
gift: [giftArr]
})
let userSchema = new Schema({
username: String,
email: { type: String, required: false },
events: [eventSchema]
});
Here are my controllers for my POST & GET routes:
export const insertEventsById = ((req, res) => {
const update = { $push: { events: req.body } }
const id = req.params.userID
Gift.findByIdAndUpdate(id, update, (err, data) => {
if (err) {
console.log(err);
} else {
res.json(data)
console.log(data);
}
})
})
export const getUserById = (req, res) => {
Gift.findById(req.params.userID, (err, user) => {
if(err){
res.send(err)
}
res.json(user)
})
}
To further illustrate, here is my postman GET request for a USER. I can push to the 'events' array (red arrow) as my findByIdAndUpdate method shows above, but when I attempt to go one nested level deeper, into the gift array (green arrow), I cannot find any documentation on that.
I been up and down the mongoose subdocuments and queries pages, and I cannot find a method that will pull specifically the '_id' of the particular event I need. I have even tried the methods on the embedded schemas to specifically look for _id's that way.
Can someone point out where I am going wrong here? Thanks in advance...as always fellow Stacks.
Related
I have a User schema with a collections property that holds collection objects. These collection objects have a property called items that is an array of objects.
//User Schema
const UserSchema = new mongoose.Schema({
username: {
type: String,
required : true,
},
password: {
type: String,
required: true
},
collections: [{type : mongoose.Schema.Types.ObjectId, ref: 'Collection'}]
});
//Collection Schema
const CollectionSchema = new mongoose.Schema({
items : [{type : mongoose.Schema.Types.ObjectId, ref: 'Item'}]
});
//Item Schema
const ItemSchema = new mongoose.Schema({
title: {
type: String,
required: true
});
Using the code below, I tried creating and pushing a new Item Schema into the first collection object. I pass the _id of the collection object I want to update using the findById function. After finding the collection with my id, I simply just push a Item Schema into the collection object's items array. I get the res.status(200) yet my items array is never updated. Does anyone else have this issue? Thank you for you help.
userRouter.post('/addItem', passport.authenticate('jwt', {session: false}), (req, res) => {
const item = new Item(req.body)
item.save(err => {
if(err)
res.status(500).json({message: {msgBody: "Error has occured", msgError: true }});
else {
Collection.findById(req.body.search, function(err, coll){
coll.items.push(item);
req.user.save(err => {
if(err)
res.status(500).json(err)
else{
res.status(200).json(coll)
}
})
})
}
})
});
You don´t perform any update operation on the Collection with the newly created Item document in the database.
Add the created Item document by using the $addToSet operator (https://docs.mongodb.com/manual/reference/operator/update/addToSet/) to the items property in Collection (I would suggest by using mongoose findByIdAndUpdate method or updateOne method).
It would be something like this using callbacks:
Collection.findByIdAndUpdate(req.body.search, { $addToSet: {items: item._id}}, callback);
If you have multiple Item documents which you need to insert into Collection, you can use $addToSet in combination with the $each operator (https://docs.mongodb.com/manual/reference/operator/update/each/).
i have this Schema for a simple twitter app
const userSchema = new Schema ({
loginInfo: {
username: String,
email: String,
password: String
},
tweets: [{
content: String,
likes: Number,
comments: [{
owner: String,
content: String,
likes: Number
}]
}],
followers: [String],
following: [String]
})
and i want to make endpoint that return only the tweet that has the same _id that has been given as a params on the URL ..
I made that solution below and its working correctly but i believe there is a much better solution than this ..
const handleTweet = (User) => (req,res) => {
const { id } = req.params;
let theTweet = [];
User.findOne({ "tweets._id": id})
.then(user => {
user.tweets.forEach(tweet => {
if(tweet._id.toString() === id)
return theTweet.push(tweet)
})
res.json(theTweet)
})
.catch(err => res.json(err))
}
module.exports = handleTweet;
One more question : Is it better to make nested schemas like this or making a different models for each schema (in this case schema for User and another one for Tweets) ?
You should make the tweets into a different collection since you are querying based on that, and then you can use autopopulate when you need it.
Also instead of the foreach you could use Array.prototype.find
Hope this helps!
You can use the $push & findOneAndUpdate methods from mongoose. You can modify your example to be like this:
User.findOneAndUpdate(id, { $push: { tweets: req.body.tweet } }, {new: true})
.then((record) => {
res.status(200).send(record);
})
.catch(() => {
throw new Error("An error occurred");
});
Notice the {new: true} option, it makes the findOneAndUpdate method to return the record with the edit.
For your second question, it's recommended to split the modals to make your code more readable, maintainable and easy to understand.
What im doing:
When I call getData() the backend server .find() all my data.
My documents:
My test document has an _id a name and stuff fields. The stuff field contains the _id to the data document.
My data document has an _id and a age field
My goal:
When I send the data to the frontend I don´t want the stuff field to appear with the _id, I want it to appear with the age field from the correspondingdata.
What I have:
router.route('/data').get((req, res) => {
Test.find((err, aval) => {
if (err)
console.log(err);
else{
var result = [];
aval.forEach(e => {
var age;
// Get the age, only 1
Data.findById(e.stuff, 'age', function (err, a) {
age = a.age;
});
result.push({name: e.name, age: age});
});
res.json(result);
}
});
});
I find all the test documents then, for each one of them, I find the age and put the result in the array. Finaly I send the result array.
My problem:
The age field on my result array is always undefined, why? Any solutions?
UPDATE 1 - The schemas
The test schema
var TestSchema = new Schema(
{
stuff: {type: Schema.Types.ObjectId, ref: 'Data', required: true},
name: {type: String, required: true}
}
);
The data schema
var DataSchema = new Schema(
{
age: {type: Number, required: true}
}
);
router.route('/data').get((req, res) => {
Test.find({})
.populate('stuff')
.exec((err, aval) => {
if (err) console.log(err);
res.json(aval);
});
});
Mongoose model has a populate property that uses the value in the model attribute definition to get data matching the _id from another model.
It's a scop problem with your code try this out :
Data.findById(e.stuff, 'age', function (err, a) {
result.push({name: e.name, age: a.age});
});
But as a better solution think to use the Aggregation Framework
Users are able to post items which other users can request. So, a user creates one item and many users can request it. So, I thought the best way would be to put an array of users into the product schema for who has requested it. And for now I just want to store that users ID and first name. Here is the schema:
const Schema = mongoose.Schema;
const productSchema = new Schema({
title: {
type: String,
required: true
},
category: {
type: String,
required: true
},
description: {
type: String,
required: true
},
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
requests: [
{
userId: {type: Object},
firstName: {type: String}
}
],
});
module.exports = mongoose.model('Product', productSchema);
In my controller I am first finding the item and then calling save().
exports.postRequest = (req, res, next) => {
const productId = req.body.productId;
const userId = req.body.userId;
const firstName = req.body.firstName;
const data = {userId: userId, firstName: firstName};
Product.findById(productId).then(product => {
product.requests.push(data);
return product
.save()
.then(() => {
res.status(200).json({ message: "success" });
})
.catch(err => {
res.status(500).json({message: 'Something went wrong'});
});
});
};
Firstly, is it okay to do it like this? I found a few posts about this but they don't find and call save, they use findByIdAndUpdate() and $push. Is it 'wrong' to do it how I have done it? This is the second way I tried it and I get the same result in the database:
exports.postRequest = (req, res, next) => {
const productId = req.body.productId;
const userId = req.body.userId;
const firstName = req.body.firstName;
const data = {userId: userId, firstName: firstName};
Product.findByIdAndUpdate(productId, {
$push: {requests: data}
})
.then(() => {
console.log('succes');
})
.catch(err => {
console.log(err);
})
};
And secondly, if you look at the screen shot is the data in the correct format and structure? I don't know why there is _id in there as well instead of just the user ID and first name.
Normally, Developers will save only the reference of other collection(users) in the collection(product). In addition, you had saved username also. Thats fine.
Both of your methods work. But, second method has been added in MongoDB exactly for your specific need. So, no harm in using second method.
There is nothing wrong doing it the way you have done it. using save after querying gives you the chance to validate some things in the data as well for one.
and you can add additional fields as well (if included in the Schema). for an example if your current json return doesn't have a field called last_name then you can add that and save the doc as well so that's a benefit..
When using findById() you don't actually have the power to make a change other than what you program it to do
One thing I noticed.. In your Schema, after you compile it using mongoose.modal()
export the compiled model so that you can use it everywhere it's required using import. like this..
const Product = module.exports = mongoose.model('Product', productSchema);
Let's suppose I have a schema like this:
var Person = new Schema({
name: String
});
var Assignment = new Schema({
name: String,
person: ObjectID
});
If I delete a person, there can still be orphaned assignments left that reference a person that does not exist, which creates extraneous clutter in the database.
Is there a simple way to ensure that when a person is deleted, all corresponding references to that person will also be deleted?
You can add your own 'remove' Mongoose middleware on the Person schema to remove that person from all other documents that reference it. In your middleware function, this is the Person document that's being removed.
Person.pre('remove', function(next) {
// Remove all the assignment docs that reference the removed person.
this.model('Assignment').remove({ person: this._id }, next);
});
If by "simple" you mean "built-in", then no. MongoDB is not a relational database after all. You need to implement your own cleaning mechanism.
The remove() method is deprecated.
So using 'remove' in your Mongoose middleware is probably not best practice anymore.
Mongoose has created updates to provide hooks for deleteMany() and deleteOne().
You can those instead.
Person.pre('deleteMany', function(next) {
var person = this;
person.model('Assignment').deleteOne({ person: person._id }, next);
});
In case if anyone looking for the pre hook but for deleteOne and deleteMany functions this is a solution that works for me:
const mongoose = require('mongoose');
...
const PersonSchema = new mongoose.Schema({
name: {type: String},
assignments: [{type: mongoose.Schema.Types.ObjectId, ref: 'Assignment'}]
});
mongoose.model('Person', PersonSchema);
....
const AssignmentSchema = new mongoose.Schema({
name: {type: String},
person: {type: mongoose.Schema.Types.ObjectId, ref: 'Person'}
});
mongoose.model('Assignment', AssignmentSchema)
...
PersonSchema.pre('deleteOne', function (next) {
const personId = this.getQuery()["_id"];
mongoose.model("Assignment").deleteMany({'person': personId}, function (err, result) {
if (err) {
console.log(`[error] ${err}`);
next(err);
} else {
console.log('success');
next();
}
});
});
Invoking deleteOne function somewhere in service:
try {
const deleted = await Person.deleteOne({_id: id});
} catch(e) {
console.error(`[error] ${e}`);
throw Error('Error occurred while deleting Person');
}
You can leave the document as is, even when the referenced person document is deleted. Mongodb clears references which point to non-existing documents, this doesn't happen immediately after deleting the referenced document. Instead, when you perform action on the document, e.g., update. Moreover, even if you query the database before the references are cleared, the return is empty, instead of null value.
Second option is to use $unset operator as shown below.
{ $unset: { person: "<person id>"} }
Note the use of person id to represent the value of the reference in the query.
you can use soft delete. Do not delete person from Person Collection instead use isDelete boolean flag to true.
Use $pull. Suppose you have a structure like this.
Stuff Collection:
_id: ObjectId('63dd23c633c17a718c4c5db7')
item: "Item 1"
user: ObjectID('63de669153bc12ecb9081b9e')
User collection:
_id: ObjectId('63de669153bc12ecb9081b9e')
stuff: array[ObjectId('63dd23c633c17a718c4c5db7'), ObjectId('63de3a69715ec134e161b0ea')]
Then after you remove the stuff:
const stuff = Stuff.findById(req.params.id)
const user = User.findById(req.params.id)
await stuff.remove()
// here you can use $pull to update
await user.updateOne({
$pull: {
stuff: stuff.id
}
})
you can simply call the model that needs to be deleted and delete that document like this:
PS: This answer is not specific to the question schema.
const Profiles = require('./profile');
userModal.pre('deleteOne', function (next) {
const userId = this.getQuery()['_id'];
try {
Profiles.deleteOne({ user: userId }, next);
} catch (err) {
next(err);
}
});
// in user delete route
exports.deleteParticularUser = async (req, res, next) => {
try {
await User.deleteOne({
_id: req.params.id,
});
return res.status(200).json('user deleted');
} catch (error) {
console.log(`error`, error);
return next(error);
}
};