mongoose sub document array change value - node.js

I got a mongoose query where I want to change a comment. I receive the commentid from a react app. But it doesn't work, what could be the problem?
An example comment array follows
"comments": [
{
"createdAt": "2018-11-22T08:28:36.881Z",
"_id": "5bf668b4001de72dc089c849", // commentid
"comment": "111111111111",
"username": "kohahn21"
},
....
]
What I have tried:
edit = await Post.update(
{ 'comments._id' : commentid },
{ '$set' : { 'comments.$.comment' : comment } },
{ new: true }
);
ctx.body = edit;
ctx.body
{
"n": 1,
"nModified": 1,
"ok": 1
}
Post Schema
const Comment = new Schema({
createdAt: { type: Date, default: Date.now },
username: String,
comment: String
});
const Post = new Schema({
username: String,
comments: {
type: [Comment],
default: []
},
});
module.exports = mongoose.model('Post',Post);
I would like to receive comments, which is a modified comment layout. What should I do?

Your syntax looks correct. But instead of 'items' it should be 'comments'.
Try Post.update( { 'comments._id' : commentid }, {'$set' : { 'comments.$.comment' : comment } });
btw the new flag is only available for find-and operators.

Related

Mongoose populate 3 deep nested schema with JSON response

I have a find() query that when executed, I can see the json with the nested schemas that I want to see except for the 'artista' attribute only displays the id, instead of the properties I want. See below:
{
"total": 1,
"ordenes": [
{
"artpieces": [
{
"_id": "60c1388f30316c02b9f6351f",
"artista": "60c055736c7ca511055a0e1a",
"nombre": "LILIES"
},
{
"_id": "60c12fca30316c02b9f63519",
"nombre": "GERNICA",
"artista": "60c136bf30316c02b9f6351b"
}
],
"_id": "60c414f9ea108a14ef75a9fb",
"precio": 3000,
"usuario": {
"_id": "609c0068e67e68",
"nombre": "Arturo Filio"
}
}
]
}
The query I use to get the json above:
const [total, ordenes] = await Promise.all([
Orden.countDocuments(),
Orden.find()
.populate("usuario", "nombre")
.populate("artpieces", ["nombre","artista","nombre"])
]);
res.json({
total,
ordenes
});
It's an order schema that has artpieces. Each artpiece (I called it 'producto'), has a name a genre, an author/artist and the user which the order belongs to.
my Schema for the orden.js:
const { Schema, model } = require('mongoose');
const OrdenSchema = Schema({
artpieces: [
{
type: Schema.Types.ObjectId,
required: true,
ref: 'Producto'
}
],
estado: {
type: Boolean,
default: true,
required: true
},
usuario: {
type: Schema.Types.ObjectId,
ref: 'Usuario',
required: true
},
precio: {
type: Number,
required: true
}
})
OrdenSchema.methods.toJSON = function () {
const { __v, estado, ...data} = this.toObject();
return data;
}
module.exports = model('Orden', OrdenSchema);
Last thing I want to mention, I know for a fact that I have the code necessary in the artista.js model to display the name of the artist because I have a similar query to display all the artpieces with each artpiece have a genre and an artist.
That example looks like so (to give context):
{
"total": 4,
"productos": [
{
"precio": 0,
"_id": "60c12fca30316c02b9f63519",
"nombre": "GERNICA",
"categoria": {
"_id": "60c04e3605d3c10ed10389e4",
"nombre": "NEO CUBISMO"
},
"artista": {
"_id": "60c136bf30316c02b9f6351b",
"nombre": "PICASSO"
},
"usuario": {
"_id": "609c8c0068e67e68",
"nombre": "Arturo Filio"
}
}
]
}
What am I doing wrong that I can't get my json result at the top look like the json at the bottom, where the artist attribute is?
Also just to point out, I have checked how to nest populate methods in order SO posts including the path and the ref to the Schema and still haven't been able to get the expected result.

How to update object array within another object array with mongoose?

I have a problem regarding MongoDB, Mongoose, and Node.js.
I have this schema using Mongoose:
const EmoteSchema = new mongoose.Schema({
guild_id: String,
users: [{
user_id: String,
emotes: [{
emote: String,
channels: [String],
cooldown: Number,
all_channels: { type: Boolean, default: true },
global_cooldown: { type: Boolean, default: true },
lock: { type: Boolean, default: false }
}]
}]
});
What it looks like MongoDB:
{
"_id" : ObjectId("5ee6776b9277898982b0a25a"),
"guild_id" : "714931095929618443",
"users" : [
{
"_id" : ObjectId("5ee6776b9277898982b0a25b"),
"user_id" : "160230784257622016",
"emotes" : [
{
"channels" : [ ],
"all_channels" : true,
"global_cooldown" : true,
"lock" : false,
"_id" : ObjectId("5ee6776b9277898982b0a25c"),
"emote" : "🔤"
}
]
}
],
"__v" : 0
}
What I'm trying to achieve is to update the database, based on three conditions:
When sent the information of the guild_id, user_id, and emote:
If the guild_id, user_id, and emote exist in the database do nothing
If the guild_id, user_id (and emote does NOT exist), update the emote array with new emote
If the guild_id, (and user_id does not exist), update the users array with new user_id and update with the emote
If guild_id does not exit: create new entry with new guild_id, users array and emotes array
This is what I have attempted to do:
let filter = {
guild_id: guild_inp,
"users.user_id": user_inp
};
let update = {
$push: { "users.$[i].emotes": { emote: emote_inp } }
};
let options = {
arrayFilters: [
{ "i.users.user_id": user_inp }
],
rawResult: true
};
Emote.findOneAndUpdate(filter, update, options, fn);
If the guild_inp: 714931095929618443, user_inp: 160230784257622016, and emote:
The callback functions gets this result:
RES: {
lastErrorObject: { n: 1, updatedExisting: true },
value: {
_id: 5ee6776b9277898982b0a25a,
guild_id: '714931095929618443',
users: [ [Object] ],
__v: 0
},
ok: 1
}
It says that it has been updated? But I see no changes in MongoDB!
Any help is appreciated if there is an easier way to achieve what I am doing such as a new schema please share it!

Nested MongoDB document issue (Mongoose and Node Js)

I am facing some issues while inserting data into nested documents structure of mongoDb.
Following is the Mongoose Model:
const funnel = new mongoose.Schema({
funnelName:{
type:String,
unique:true
},
group: String,
category: String,
funnelStep: {
stepType: String,
stepName: String,
stepPath: String,
isTracking: Boolean,
viewsStorage: []
} })
Below is the push I am sending to Db:
router.post('/createFunnel',async (req,res)=>{
if(!req.body.funnelName || !req.body.group || !req.body.category)
{return res.status(422).json({error:"Please add all the fields."})}
try{
const funnelSteps = []
funnelSteps.push({
stepType: req.body.stepType,
stepName: req.body.stepName,
stepPath: req.body.stepPath,
isTracking: req.body.isTracking,
viewsStorage: req.body.viewsStorage
})
const funnels = new Funnel({
funnelName : req.body.funnelName,
group : req.body.group,
category : req.body.category,
funnelStep : funnelSteps
})
await funnels.save(function(err){
if(err){
return res.status(422).send({error: err.message})
}
return res.json(funnels)
})
} catch(err){
return res.status(422).send({error: err.message})
} })
Below is the data structure I am sending through postman:
{
"funnelName":"Name-Funnel",
"group":"AVC",
"category":"XYZ",
"funnelStep":[
{
"stepType":"Advert",
"stepName":"Angle",
"stepPath":"google.com",
"isTracking":1,
"viewsStorage":[0,0]
},
{
"stepType":"Optin",
"stepName":"Ver 1",
"stepPath":"fb.com",
"isTracking":1,
"viewsStorage":[1,0]
},
{
"stepType":"Check",
"stepName":"rev-cat",
"stepPath":"google.com",
"isTracking":0,
"viewsStorage":[2,0]
}
] }
Below is the output I am getting in response:
{
"funnelStep": {
"viewsStorage": []
},
"_id": "5ec0ff78a6dfab18f4210e96",
"funnelName": "Testing The Latest Method4",
"group": "AVC",
"category": "XYZ",
"__v": 0
}
How can I fix this issue as my data is not getting inserted properly?
And apart from this, in the viewsStorage array, how to store date and a number which will increment after a certain operations and will get saved in the array according to the dates?
I think there is an issue in the funnelSteps array creation part. You are trying to get data directly from req.body instead of req.body.funnelStep
const funnelSteps = []
req.body.funnelStep.forEach(fs => {
funnelSteps.push({
stepType: fs.stepType,
stepName: fs.stepName,
stepPath: fs.stepPath,
isTracking: fs.isTracking,
viewsStorage: fs.viewsStorage
})
})
Schema
const funnel = new mongoose.Schema({
funnelName:{
type:String,
unique:true
},
group: String,
category: String,
funnelStep: [{
stepType: String,
stepName: String,
stepPath: String,
isTracking: Boolean,
viewsStorage: []
}] })

Mongodb update multiple documents with different values

I have been trying to use updatemany with mongoose. I want to update the values in database using an array of objects.
[
{
"variantId": "5e1760fbdfaf28038242d676",
"quantity": 5
},
{
"variantId": "5e17e67b73a34d53160c7252",
"quantity": 13
}
]
I want to use variantId as filter.
Model schema is:
let variantSchema = new mongoose.Schema({
variantName: String,
stocks: {
type: Number,
min: 0
},
regularPrice: {
type: Number,
required: true
},
salePrice: {
type: Number,
required: true
}
})
I want to filter the models using variantId and then decrease the stocks.
As you need to update multiple documents with multiple criteria then .updateMany() wouldn't work - it will work only if you need to update multiple documents with same value, Try this below query which will help you to get it done in one DB call :
const Mongoose = require("mongoose");
let variantSchema = new mongoose.Schema({
variantName: String,
stocks: {
type: Number,
min: 0
},
regularPrice: {
type: Number,
required: true
},
salePrice: {
type: Number,
required: true
}
})
const Variant = mongoose.model('variant', variantSchema, 'variant');
let input = [
{
"variantId": "5e1760fbdfaf28038242d676",
"quantity": 5
},
{
"variantId": "5e17e67b73a34d53160c7252",
"quantity": 13
}
]
let bulkArr = [];
for (const i of input) {
bulkArr.push({
updateOne: {
"filter": { "_id": Mongoose.Types.ObjectId(i.variantId) },
"update": { $inc: { "stocks": - i.quantity } }
}
})
}
Variant.bulkWrite(bulkArr)
Ref : MongoDB-bulkWrite
I don't think this can be done with a single Model.updateMany query. You will need to loop the array and use Model.update instead.
for (const { variantId, quantity } of objects) {
Model.update({ _id: variantId }, { $inc: { stocks: -quantity } });
}
To run this in a transaction (https://mongoosejs.com/docs/transactions.html), the code should look something like this (however I have not tried or tested this):
mongoose.startSession().then(async session => {
session.startTransaction();
for (const { variantId, quantity } of objects) {
await Model.update({ _id: variantId }, { $inc: { stocks: -quantity } }, { session });
}
await session.commitTransaction();
});

Upsert Using an Array Without Creating Duplicates

I'm having trouble 'upserting' to my array. The code below creates duplicates in my answers array which I definitely do not want and by now it's apparent $push will not work. I have tried using the different methodologies I see on SO for a while now but none are working for me. With this web app, users are allows to view a question on the website and respond with a 'yes' or 'no' response and they are allowed to change(upsert) their response at any one time meaning a sort of upsert takes place on the db at different times. How do get around this?
var QuestionSchema = Schema ({
title :String,
admin :{type: String, ref: 'User'},
answers :[{type: Schema.Types.Mixed, ref: 'Answer'}]
});
var AnswerSchema = Schema ({
_question :{type: ObjectId, ref: 'Question'},
employee :{type: String, ref: 'User'},
response :String,
isAdmin :{type: Boolean, ref: 'User'}
})
var UserSchema = Schema({
username : String,
isAdmin : {type: Boolean, default: false}
});
module.exports = mongoose.model('Question', QuestionSchema);
module.exports = mongoose.model('Answer', AnswerSchema);
module.exports = mongoose.model('User', UserSchema);
Question.update(
{_id: req.body.id},
{$push: {answers: {_question: req.body.id,
employee: req.body.employee,
response: req.body.response, //this variable changes (yes/no/null)
isAdmin: req.body.isAdmin}}},
{safe: true, upsert: true},
function(err, model) {
}
);
As I see it you seem a little confused and it's reflected in your schema. You don't seem to fully grasp the differences between "embedded" and "referenced" since your schema is actually an invalid "mash" of the two techniques.
Probably best to walk you through both of them.
Embedded Model
So instead of the schema you have defined, you should in fact have something more like this:
var QuestionSchema = Schema ({
title :String,
admin :{type: String, ref: 'User'},
answers :[AnswerSchema]
});
var AnswerSchema = Schema ({
employee :{type: String, ref: 'User'},
response :String,
isAdmin :{type: Boolean, ref: 'User'}
})
mongoose.model('Question', questionSchema);
NOTE: Question is the only actual model here. The AnswerSchema is completely "embedded".
Note the clear definition of the "schema" where the "answers" property in Question is defined as an "array" of AnswerSchema. This is how you do embedding and keep control of the types within the object inside the array.
As for the update, there is a clear logic pattern but you are simply not enforcing it. All you need to do is "tell" the update that you do not want to "push" a new item if something for that "unique" "employee" in the array already exists.
Also. This is NOT and "upsert". Upsert implies "creating a new one", which is different to what you want. You want to "push" to the array of an "existing" Question. If you leave "upsert" on there, then something not found creates a new Question. Which is of course wrong here.
Question.update(
{
"_id": req.body.id,
"answers.employee": { "$ne": req.body.employee },
}
},
{ "$push": {
"answers": {
"employee": req.body.employee,
"response": req.body.response,
"isAdmin": req.body.isAdmin
}
}},
function(err, numAffected) {
});
That will look to check that the "unique" "employee" in the array members already and will only $push where it is not already there.
As a bonus, if you intend to allow the user to "change their answer" then we do this incantation with .bulkWrite():
Question.collection.bulkWrite(
[
{ "updateOne": {
"filter": {
"_id": req.body.id,
"answers.employee": req.body.employee,
},
"update": {
"$set": {
"answers.$.response": req.body.response,
}
}
}},
{ "updateOne": {
"filter": {
"_id": req.body.id,
"answers.employee": { "$ne": req.body.employee },
},
"update": {
"$push": {
"answers": {
"employee": req.body.employee,
"response": req.body.response,
"isAdmin": req.body.isAdmin
}
}
}
}}
],
function(err, writeResult) {
}
);
This effectively puts two updates in one. The first to attempt to alter an existing answer and $set the response at the matched position, and the second to attempt to add a new answer where one was not found on the question.
Referenced Model
With a "referenced" model you actually have the real members of the Answer within their own collection. So instead the schema is defined like this:
var QuestionSchema = Schema ({
title :String,
admin :{type: String, ref: 'User'},
answers :[{ type: Schema.Types.ObjectId, ref: 'Answer' }]
});
var AnswerSchema = Schema ({
_question :{type: ObjectId, ref: 'Question'},
employee :{type: String, ref: 'User'},
response :String,
isAdmin :{type: Boolean, ref: 'User'}
})
mongoose.model('Answer', answerSchema);
mongoose.model('Question', questionSchema);
N.B The other ref's here to User such as :
employee :{type: String, ref: 'User'},
isAdmin :{type: Boolean, ref: 'User'}
These are also really incorrect, and also should be of Schema.Type.ObjectId as they will "reference" the actual _id field of User. But this is actually outside of the scope of this question as asked, so if you still don't grasp that after this read, then Ask a New Question so someone can explain. On with the rest of the answer.
That's the "general" shape of the schema though, with the important thing being the "ref" to the 'Anwser' "model", which is by the registered name. You can optionally just use your "_question" field in modern mongoose versions with a "virtual", but I'm skipping over "Adavanced Usage" for now and keeping it simple with an array of "references" still in the Question model.
In this case, since the Answer model is actually in it's own "collection", then the operations actually become "upserts". Where we only want to "create" when there is no "employee" response to the given "_question" id.
Also demonstrating with a Promise chain instead:
Answer.update(
{ "_question": req.body.id, "employee": req.body.employee },
{
"$set": {
"response": req.body.reponse
},
"$setOnInsert": {
"isAdmin": req.body.isAdmin
}
},
{ "upsert": true }
).then(resp => {
if ( resp.hasOwnProperty("upserted") ) {
return Question.update(
{ "_id": req.body.id, "answers": { "$ne": resp.upserted[0]._id },
{ "$push": { "answers": resp.upserted[0]._id } }
).exec()
}
return;
}).then(resp => {
// either undefined where it was not an upsert or
// the update result from Question where it was
}).catch(err => {
// something
})
This is actually a simple statement since "when matched" we want to change the "response" data with the payload of the request, and really only when "upserting" or "creating/inserting" is when we actually change other data such as the "employee" ( which is always implied for create as part of the query expression ) and the "isAdmin" which clearly should not change with each update request we then explicitly use $setOnInsert so it only writes those two fields on an actual "create".
In the "Promise Chain" we actually look to see if the update request to Answer actually resulted in an "upsert", and when it does we want to append to the array of Question where it does not already exist. In much the same way as the "embedded" example, it's best to look to see if the array actually has the item before modifying with the "update". Alternately you could $addToSet here and just let the query match the Question by _id. To me though, that's a wasted write.
Summary
Those are your different approaches to how you handle this. Each has their own use cases for which you can see a general summary some other answers of mine in:
Mongoose populate vs object nesting which is an overview of the different approaches and reasons behind them
How to Model a “likes” voting system with MongoDB which gives a bit more detail on the "unique array upserts" technique for "embedded" models.
Not "required" reading, but it may help expand your insight into which approach is best for your particular case.
Working Example
Copy these and put them in a directory and do an npm install to install local dependencies. The code will run and create the collections in the database making the alterations.
Logging is turned on with mongoose.set(debug,true) so you should look at the console output and see what it does, along with the resulting collections where answers will be recorded to the related questions, and overwritten instead of "duplicating" where that was also the intent.
Change the connection string if you have to. But that is all you should change in this listing for it's purpose. Both approaches described in the answer are demonstrated.
package.json
{
"name": "colex",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"async": "^2.4.1",
"mongodb": "^2.2.29",
"mongoose": "^4.10.7"
}
}
index.js
const async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema,
ObjectId = require('mongodb').ObjectID
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
mongoose.connect('mongodb://localhost/coltest');
const userSchema = new Schema({
username: String,
isAdmin: { type: Boolean, default: false }
});
const answerSchemaA = new Schema({
employee: { type: Schema.Types.ObjectId, ref: 'User' },
response: String,
});
const answerSchemaB = new Schema({
question: { type: Schema.Types.ObjectId, ref: 'QuestionB' },
employee: { type: Schema.Types.ObjectId, ref: 'User' },
response: String,
});
const questionSchemaA = new Schema({
title: String,
admin: { type: Schema.Types.ObjectId, ref: 'User' },
answers: [answerSchemaA]
});
const questionSchemaB = new Schema({
title: String,
admin: { type: Schema.Types.ObjectId, ref: 'User' },
answers: [{ type: Schema.Types.ObjectId, ref: 'AnswerB' }]
});
const User = mongoose.model('User', userSchema);
const AnswerB = mongoose.model('AnswerB', answerSchemaB);
const QuestionA = mongoose.model('QuestionA', questionSchemaA);
const QuestionB = mongoose.model('QuestionB', questionSchemaB);
async.series(
[
// Clear data
(callback) => async.each(mongoose.models,(model,callback) =>
model.remove({},callback),callback),
// Create some data
(callback) =>
async.each([
{
"model": "User",
"object": {
"_id": "594a322619ddbd437193c759",
"name": "Admin",
"isAdmin": true
}
},
{
"model": "User",
"object": {
"_id": "594a323919ddbd437193c75a",
"name": "Bill"
}
},
{
"model": "User",
"object": {
"_id": "594a327b19ddbd437193c75b",
"name": "Ted"
}
},
{
"model": "QuestionA",
"object": {
"_id": "594a32f719ddbd437193c75c",
"admin": "594a322619ddbd437193c759",
"title": "Question A Model"
}
},
{
"model": "QuestionB",
"object": {
"_id": "594a32f719ddbd437193c75c",
"admin": "594a322619ddbd437193c759",
"title": "Question B Model"
}
}
],(data,callback) => mongoose.model(data.model)
.create(data.object,callback),
callback
),
// Submit Answers for Users - Question A
(callback) =>
async.eachSeries(
[
{
"_id": "594a32f719ddbd437193c75c",
"employee": "594a323919ddbd437193c75a",
"response": "Bills Answer"
},
{
"_id": "594a32f719ddbd437193c75c",
"employee": "594a327b19ddbd437193c75b",
"response": "Teds Answer"
},
{
"_id": "594a32f719ddbd437193c75c",
"employee": "594a323919ddbd437193c75a",
"response": "Bills Changed Answer"
}
].map(d => ([
{ "updateOne": {
"filter": {
"_id": ObjectId(d._id),
"answers.employee": ObjectId(d.employee)
},
"update": {
"$set": { "answers.$.response": d.response }
}
}},
{ "updateOne": {
"filter": {
"_id": ObjectId(d._id),
"answers.employee": { "$ne": ObjectId(d.employee) }
},
"update": {
"$push": {
"answers": {
"employee": ObjectId(d.employee),
"response": d.response
}
}
}
}}
])),
(data,callback) => QuestionA.collection.bulkWrite(data,callback),
callback
),
// Submit Answers for Users - Question A
(callback) =>
async.eachSeries(
[
{
"_id": "594a32f719ddbd437193c75c",
"employee": "594a323919ddbd437193c75a",
"response": "Bills Answer"
},
{
"_id": "594a32f719ddbd437193c75c",
"employee": "594a327b19ddbd437193c75b",
"response": "Teds Anwser"
},
{
"_id": "594a32f719ddbd437193c75c",
"employee": "594a327b19ddbd437193c75b",
"response": "Ted Changed it"
}
],
(data,callback) => {
AnswerB.update(
{ "question": data._id, "employee": data.employee },
{ "$set": { "response": data.response } },
{ "upsert": true }
).then(resp => {
console.log(resp);
if (resp.hasOwnProperty("upserted")) {
return QuestionB.update(
{ "_id": data._id, "employee": { "$ne": data.employee } },
{ "$push": { "answers": resp.upserted[0]._id } }
).exec()
}
return;
}).then(() => callback(null))
.catch(err => callback(err))
},
callback
)
],
(err) => {
if (err) throw err;
mongoose.disconnect();
}
)
Here was my quick work around before Neill updated his answer (I used a $pull & $push). Works just as his but I'll mark his correct as I believe it's more efficient.
Question.update(
{_id: req.body.id},
{$pull: {answers: { employee: req.body.employee}}},
{safe: true, multi:true, upsert: true},
function(err, model) {
}
);
Question.update(
{_id: req.body.id},
{$push: {answers: {_question: req.body.id,
employee: req.body.employee,
response: req.body.response,
isAdmin: req.body.isAdmin}}},
{safe: true, upsert: true},
function(err, model) {
}
);

Resources