MongoDB array of objects update - node.js

I'm trying to update array of user information objects in mongoose.
I've stored the user core information in the login process, I want to update some of user information when user tries to make an order.
Here is the code for the model
const mongoose = require('mongoose');
const { ObjectId } = mongoose.Schema;
const userSchema = new mongoose.Schema(
{
name: String,
email: {
type: String,
required: true,
index: true,
},
role: {
type: String,
default: 'subscriber',
},
info: [
{ country: String },
{ city: String },
{ address: String },
{ phone: String },
{ birthdate: Date },
{ gender: { type: String, enum: ['Male', 'Female'] } },
],
// wishlist: [{ type: ObjectId, ref: "Product" }],
},
{ timestamps: true }
);
module.exports = mongoose.model('User', userSchema);
In my controller I'm getting the data from front-end react app as JSON format, I want to push some data to info which is an array of objects in the users model above.
exports.createOrder = async (req, res) => {
// Here I constract the data
const { plan, service, fullName, country, city, address } = req.body.order;
const { user_id } = req.body;
// This the method I tried
try {
const user = await User.updateOne(
{
_id: user_id,
},
{
$set: {
'info.$.country': country,
'info.$.city': city,
'info.$.address': address,
},
},
{ new: true }
);
if (user) {
console.log('USER UPDATED', user);
res.json(user);
} else {
res.json((err) => {
console.log(err);
});
}
const newOrder = await new Order({
orderPlan: plan,
orderService: service,
orderUser: user_id,
}).save();
console.log(newOrder);
console.log(req.body);
} catch (error) {
console.log(error);
}
};
I tired other solutions like
const user = await User.updateOne(
{
_id: user_id,
info: { $elemMatch: { country, city, address } },
},
{ new: true }
);
So do I need to reformat my model or there is a way to update this array of objects?

Option 1
Use $[]
db.collection.update(
{},
{ $set: { "info.$[i].country": "a1" }} ,
{ arrayFilters: [ { "i.country": "a" } ] }
)
Demo - https://mongoplayground.net/p/UMxdpyiKpa9
Option 2
if you know the index
Demo - https://mongoplayground.net/p/41S7qs6cYPT
db.collection.update({},
{
$set: {
"info.0.country": "a1",
"info.1.city": "b1",
"info.2.address": "c1",
"info.3.phone": "d1"
}
})
Suggestions -
Change info schema to object instead of an array

Related

How do I update nested array values in mongoose?

I am fairly new to nodejs/express and I'm practicing full stack development with devchallenges.io, i'm doing the shoppingify challenge. I'm trying to update the quantity of an item I am targeting inside of the items array. I understand my attempt below was terrible, I'm really struggling to understand the logic to be able to do so.
// #route PUT api/list/item/quantity/:id
// #desc Increase or decrease quantity
// #access Private
router.put('/item/quantity/:id', auth, async (req, res) => {
const { action } = req.body;
try {
let list = await List.findOne({ user: req.user.id });
const item = list.items.find(
(item) => item._id.toString() === req.params.id
);
list = list.updateOne(
{ 'items._id': req.params.id },
{ $set: { 'items.quantity': item.quantity + 1 } }
);
await list.save();
return res.json(list);
} catch (error) {
console.error(error.message);
res.status(500).send('Server Error');
}
});
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const ListSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
},
name: {
type: String,
default: 'Shopping List',
},
items: [
{
name: {
type: String,
default: '',
},
note: {
type: String,
default: '',
},
image: {
type: String,
default: '',
},
category: {
type: String,
default: '',
},
quantity: {
type: Number,
default: 1,
},
},
],
date: {
type: Date,
default: Date.now,
},
});
module.exports = List = mongoose.model('list', ListSchema);
Look this is my update-vendor route here I'm updating nested street and city name.
router.put("/update-vendors", async (req, res, next) => {
const vendor = await Vendor.updateOne(
{
"address.street": "Street2",
},
{
$set: {
"address.$.street": req.body.street,
"address.$.city": req.body.city,
},
}
);
res.status(200).json(vendor);
});
You can update particular things with the help of $set and other $push method

updating subdocument array in mongoose express

So I have a schema, which is like this:
const playerSchema = new Schema(
{
user: {
type: Schema.Types.ObjectId,
ref: 'user'
},
paid : {type: Boolean, default: false}
}
)
const tournamentSchema = new Schema(
{
name: {
type: String,
required: true
},
player:[ playerSchema])
so in the tournament model i get this as return:
{
"_id": "5fbf3afe1ecd92296c746db6",
"name": "1st Testing Tournament",
"player": [
{
"paid": false,
"_id": "5fbf3cfe6347784028da8181",
"user": "5fbf3b4c1ecd92296c746dcd"
}
]}
in the API I will have only the user ID and the tournament ID. I want to update paid in players array from false to true. Here is what I tried:
exports.put_confirmPayment= async(req, res)=>{
const uid = req.params.user_id
const tid = req.params.tid
const findData= {"_id": tid, "player.user": uid }
const changeData = {"player.$.paid": "true"}
try {
await Tournament.findOneAndUpdate( findData, {$set: {changeData}} )
const tDB = await Tournament.findById(tid)
res.status(200).json(tDB)
} catch (error) {
console.log(error)
res.status(500).json(error.message)
}
}
Where I am going wrong? and what should my approach be?
convert string id from string to object type using mongoose.Types.ObjectId
change "true" string to boolean true
return updated result using returnOriginal: false, or new: true both will return new updated result
have removed extra constant variables, don't create too much variables
exports.put_confirmPayment = async(req, res) => {
try {
const tDB = await Tournament.findOneAndUpdate(
{
_id: mongoose.Types.ObjectId(req.params.tid),
"player.user": mongoose.Types.ObjectId(req.params.user_id)
},
{ $set: { "player.$.paid": true } },
{ returnOriginal: false }
);
res.status(200).json(tDB);
} catch (error) {
console.log(error);
res.status(500).json(error.message);
}
}
Playground
For more information refer mongoose findoneandupdate documentation.

Nested populate array of ObjectId

I'm trying to populate array of ObjectId in mongoose. And inside that array, will need populate again.
Let's say I've User data schema as below:-
models/User.js
/** Dependencies */
// Mongoose
const mongoose = require('mongoose')
/** Data Schema */
// User Data Schema
const UserSchema = new mongoose.Schema({
// User's Name
name: {
// User's Firstname
firstName: { type: String, required: true, min: 4 },
// User's Lastname
lastName: { type: String, required: true, min: 4 }
},
// User's Address Array[ID]
address: [{
// AddressID
addressID: { type: mongoose.Schema.Types.ObjectId, ref: "Address" },
}]
})
/** Data Schema Exports */
module.exports = mongoose.model('User', UserSchema);
and Address schema, models/Address.js
/** Dependencies */
// Mongoose
const mongoose = require('mongoose')
/** Data Schema */
// Address Data Schema
const AddressSchema = new mongoose.Schema({
// Address 1
addressOne: { type: String, required: true },
// Address 2
addressTwo: { type: String },
// Postcode
postcode: { type: String, required: true },
// City
city: { type: String, required: true },
// StateID
state: { type: mongoose.Schema.Types.ObjectId, ref: "State" },
// CountryID
country: { type: mongoose.Schema.Types.ObjectId, ref: "Country" }
})
// State Data Schema
const StateSchema = new mongoose.Schema({
// Gender's Name
name: { type: String, required: true }
})
// Country Data Schema
const CountrySchema = new mongoose.Schema({
// Race's Name
name: { type: String, required: true }
})
/** Data Schema Export */
const Address = mongoose.model('Address', AddressSchema)
const State = mongoose.model('State', StateSchema)
const Country = mongoose.model('Country', CountrySchema)
module.exports = {
Address,
State,
Country
}
I know how to populate state & country from Address, like show below:-
Address.find()
.populate('state')
.populate('country')
.then(async address => {
// do stuff
})
.catch(err => res.json({ err }))
But how can I populate array of ObjectId. I did the code like shown below:-
User.findById({ _id: userId }) // let's say I've the userId
.populate('address')
.then(async user => {
console.log(await user)
})
.catch(err => res.json({ err }))
Unfortunately, It returns me something like this:-
{
"_id": "5fabababababababababab1"
"name": {
"firstName": "Lalapolalaa",
"lastName": "Newb"
},
"address": [
{
"_id": "5fcdcdcdcdcdcdcdcdc1",
"addressID": "5fefefefefefefefefef1" // should've populate (but not working)
}
],
"__v": 0
}
What I'm trying to get is like shown below:-
{
"_id": "5fabababababababababab1"
"name": {
"firstName": "Lalapolalaa",
"lastName": "Newb"
},
"address": [
{
"_id": "5fcdcdcdcdcdcdcdcdc1",
"addressID": { // populate happens here
"_id": "5fefefefefefefefefef1",
"addressOne": "Lot 1, Street 12",
"addressTwo": "SS 21",
"postcode" : "47500",
"city": "Subang Jaya",
"state": { // populate happens here
"_id": "5fghghghghghghghghg1",
"name": "Selangor",
"__v": 0
},
"country": { // populate happens here
"_id": "5ijijijijijijijijij1",
"name": "Malaysia",
"__v": 0
}
"__v": 0
}
}
],
"__v": 0
}
How can I get that (shown above) with my current code below:-
User.findById({ _id: userId }) // let's say I've the userId
.populate('address') // assuming this populate of address works
.then(async user => {
// how to populate state & country of Address?
// code below only return address[0] information (assumption)
const address = await Address.findById({ _id: user.address[0].addressID._id })
.populate('state')
.populate('country')
.then(address => address)
})
.catch(err => res.json({ err }))
I'm able to find the solution myself with below corrections:-
in models/User.js:-
/** Dependencies */
// Mongoose
const mongoose = require('mongoose')
/** Data Schema */
// User Data Schema
const UserSchema = new mongoose.Schema({
// User's Name
name: {
// User's Firstname
firstName: { type: String, required: true, min: 4 },
// User's Lastname
lastName: { type: String, required: true, min: 4 }
},
// User's Address Array[ID]
address: [{ type: mongoose.Schema.Types.ObjectId, ref: "Address" }] // 1st Correction
})
/** Data Schema Exports */
module.exports = mongoose.model('User', UserSchema);
Then next I can straight code like shown below (SOLUTION):-
User.findById({ _id: userId }) // let's say I've the userId
.populate('address') // assuming this populate of address works
.then(async user => {
// create an array to hold JUST the IDs of Address available
let addressesID = []
// transfer all Address's ID
for(let i = 0; i < (user.address).length; i++) {
// push available address's ID one by one
addressesID.push(user.address[i]._id)
}
// get all address info available in Address (from DB)
const addresses = await Address.find(
{ _id: { $in: addressesID } }
)
.populate('state')
.populate('country')
.then(address => address)
})
.then(all => console.log(all))
.catch(err => res.json({ err }))
it will then console.log()
{
_id: '5fabababababababababab1',
name: {
firstName: 'Lalapolalaa',
lastName: 'Newb'
},
address: { // populate happens here
_id: '5fefefefefefefefefef1',
addressOne: 'Lot 1, Street 12',
addressTwo: 'SS 21',
postcode" : '47500',
city: 'Subang Jaya',
state: { // populate happens here
_id: '5fghghghghghghghghg1',
name: 'Selangor',
__v: 0
},
country: { // populate happens here
_id: '5ijijijijijijijijij1',
name: 'Malaysia',
__v: 0
},
__v: 0
},
__v: 0
}

Mongoose: how to only populate, sort and return a nested object?

I have a User schema, with a messages array. The message array is filled by conversations id and referenced to a Conversation schema.
I want to fetch all conversations from a user, sort them by unread and then most recent messages. Finally, I must only return an array of lastMessage object.
For the moment, I have only managed to populate the whole user object.
Here is the Conversation Schema:
const conversationSchema = new mongoose.Schema(
{
name: { type: String, required: true, unique: true },
messages: [{ message: { type: String }, authorId: { type: String } }],
lastMessage: {
authorId: { type: String },
snippet: { type: String },
read: { type: Boolean },
},
},
{ timestamps: true }
);
conversationSchema.index({ name: 1 });
module.exports = mongoose.model("Conversation", conversationSchema);
And here is my code:
router.get("/conversations", async (req, res) => {
try {
const { userId } = req.query;
const user = await User.findById({ _id: userId }).populate("messages");
.sort({ updatedAt: 1, "lastMessage.read": 1 });
return res.json({ messages: user.messages });
} catch (err) {
console.log("error", err);
return res.json({ errorType: "unread-messages-list" });
}
});
How to do this?

Accessing a schema inside a schema using Express Router and MongoDG

I'm trying to create a route where it takes in a parameter for a username and then displays that users information. Only thing is, the username is in the user schema from when the user signs up. The profile schema references the user schema. How do I use the username parameter in the findOne call to display the users profile data?
User schema:
const UserSchema = new Schema({
username: {
type: String,
required: true
},
email: {
type: String,
required: true
},
password: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now
}
});
module.exports = User = mongoose.model("users", UserSchema);
Profile schema:
const ProfileSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: "users"
},
name: {
type: String
},
image: {
type: String
},
bio: {
type: String
},
location: {
type: String
},
website: {
type: String
},
social: {
youtube: {
type: String
},
facebook: {
type: String
},
instagram: {
type: String
},
twitter: {
type: String
}
}
});
module.exports = User = mongoose.model("profile", ProfileSchema);
Route:
router.get("/user/:username", (req, res) => {
const errors = {};
Profile.findOne({ user: req.params.user.username })
.populate("user", "username")
.then(profile => {
if (!profile) {
errors.noprofile = "There is no profile for this user";
return res.status(404).json(errors);
}
res.json(profile);
})
.catch(err => res.status(404).json(err));
});
Please try this :
router.get("/user/:username", async (req, res) => {
const errors = {};
try {
const profile = await User.aggregate([
{ $match: { username: req.params.username } },
{ $lookup: { from: "profile", localField: "_id", foreignField: "user", as: "userProfile" } },
{ $project: { userProfile: { $arrayElemAt: ["$userProfile", 0] }, username: 1, _id:0 } }
]).exec();
if (!profile.length) {
errors.noprofile = "There is no profile for this user";
return res.status(404).json(errors);
}
res.json(profile[0]);
} catch (error) {
console.log('Error in retrieving user from DB ::', error);
return res.status(404);
}
})
Try using aggregate, firstly you check-in user table for getting details of a specific username then fetch the profile details as below using lookup, if no profile found after unwind the document will not be fetched and you can check on aggregate result's length as aggregate always return an array in result :
User.aggregate([
{$match:{ username: req.params.user.username }},
{$lookup:{from:"profile",localField:"_id",foreignField:"userId",as:"profileData"}},
{$unwind:"$profileData"},
{$project:{profileData:1,username:1}}
{$limit:1}
])
.then(profile => {
if (!profile.length) {
errors.noprofile = "There is no profile for this user";
return res.status(404).json(errors);
}
res.json(profile[0]);
})
You can do it in 2 steps.
Look for users containing username in userSchema, get it's id.
Then in promise, use that id to, look for profileSchema contains.
router.get("/user/:username", (req, res) => {
users.findOne({ username: req.params.username }).then(_user=>{
profile.findOne({ user: _user._id }).populate('user').then(_profile => {
res.json(_profile);
})
})
});
This code will look for username in userSchema and look for userSchema's id in profileSchema then returns profileSchema populated with user.

Resources