MongoError unknown top level operator: $set - mongodb

When I do this:
return scores.updateQ({_id:score._id},
{
$set:{"partId":partId,"activityId":activityId},
$unset:{topicType:'',topicId:'',courseId:''}
},
{strict:false})
Where partId and activityId are variables, defined elsewhere,
I get
{ name: 'MongoError',
message: 'unknown top level operator: $set',
index: 0,
code: 2,
errmsg: 'unknown top level operator: $set' }
This answer says
"The "unknown top level operator" message means that the operator has
to appear after the field to which it applies."
But the documentation says you can do it the way I'm doing it
So maybe there's something else wrong?

The problem is that you're using the syntax for the wrong update method. You should be using this method's syntax, assuming that scores is a document.
return scores.updateQ({
$set: { "partId": partId, "activityId": activityId},
$unset: { topicType: '', topicId: '', courseId: ''}
},
{ strict: false });
Also, in Mongoose, it uses $set by default, so this should be equivalent:
return scores.updateQ({
partId: partId,
activityId: activityId,
$unset: { topicType: '', topicId: '', courseId: ''}
},
{ strict: false });
EDIT:
My assumption is that scores is a document (an instance of the Model):
var schema = new Schema({});
var Scores = mongoose.model('Scores', schema);
var scores = new Scores({});
Both Scores.update and scores.update exist, but the syntax is different, which may be what's causing your error. Here's the difference:
// Generic update
Scores.update({ _id: id }, { prop: 'value' }, callback);
// Designed to update scores specifically
scores.update({ prop: 'value' }, callback);
NOTE:
If these assumptions are not correct, include more context in your answer, like how you got there.

Related

Is something missing in these positional arguments for nested array updates?

I have a document that has 3 nested arrays. I'm updating the second nested array with this function:
// Append $set key
for(var key in u)
updates["scopes.$[].sections.$." + key] = u[key];
Proposal.findOneAndUpdate({
"scopes.sections._id": req.body.id // sectionId
}, {
$set: updates
}, { new: true })
.then(response => {
console.log(response);
})
.catch(error => {
console.log(error);
});
I use a similar function to update the first nested array- scopes. That is working properly and updates the scope that matches. But for the second nested array only the first element of the array is being updated. I logged the id and the correct param is being passed in the req.body.
Is there something I'm missing in the update key- scopes.$[].sections.$.key ?
Edit with sample document and logs-
_id: 6079c199c5464b6296b113f6
name: ""
status: "outstanding"
hasAutomaticThreshold:false
isDiscount:true
discount: 0
discountPercentage: 0
taxRate: 9
companyId: 606f5e179cc0382ad6aacd84
clientId: 6070fa06dd505146ccfac9ec
projectId: 60736ed48fb2c869e0c9b33d
author: 606f5e259cc0382ad6aacd86
scopes: Array
0:Object
title: ""
isExpanded: true
_id: 6079c199c5464b6296b113f7
sections:Array
0:Object
title:"Section One"
description:""
isExpanded:false
_id: 6079c199c5464b6296b113f8
items: Array
1:Object
title:""
description:""
isExpanded:false
_id: 6079c1f8d3176462c0840388
items: Array
And this is what the logged req.body.id and updates object looks like:
6079c1f8d3176462c0840388 // ID
{ 'scopes.$[].sections.$.title': 'Section One' }
The positional $ operator will update single position, you need to use arrayFilters $[<identifier>],
// Append $set key
for(var key in u)
updates["scopes.$[].sections.$[s]." + key] = u[key];
Proposal.findOneAndUpdate(
{ "scopes.sections._id": req.body.id },
{ $set: updates },
{
arrayFilters: [{ "s._id": req.body.id }],
new: true
}
)
.then(response => {
console.log(response);
})
.catch(error => {
console.log(error);
});
Playground

Update query adding ObjectIDs to array twice

I am working on a table planner application where guests can be assigned to tables. The table model has the following Schema:
const mongoose = require('mongoose');
mongoose.Promise = global.Promise;
const tableSchema = new mongoose.Schema({
name: {
type: String,
required: 'Please provide the name of the table',
trim: true,
},
capacity: {
type: Number,
required: 'Please provide the capacity of the table',
},
guests: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Guest',
}],
});
module.exports = mongoose.model('Table', tableSchema);
Guests can be dragged and dropped in the App (using React DND) to "Table" React components. Upon being dropped on a table, an Axios POST request is made to a Node.js method to update the Database and add the guest's Object ID to an array within the Table model:
exports.updateTableGuests = async (req, res) => {
console.log(req.body.guestId);
await Table.findOneAndUpdate(
{ name: req.body.tablename },
{ $push: { guests: req.body.guestId } },
{ safe: true, upsert: true },
(err) => {
if (err) {
console.log(err);
} else {
// do stuff
}
},
);
res.send('back');
};
This is working as expected, except that with each dropped guest, the Table model's guests array is updated with the same guest Object ID twice? Does anyone know why this would be?
I have tried logging the req.body.guestID to ensure that it is a single value and also to check that this function is not being called twice. But neither of those tests brought unexpected results. I therefore suspect something is wrong with my findOneAndUpdate query?
Don't use $push operator here, you need to use $addToSet operator instead...
The $push operator can update the array with same value many times
where as The $addToSet operator adds a value to an array unless the
value is already present.
exports.updateTableGuests = async (req, res) => {
console.log(req.body.guestId);
await Table.findOneAndUpdate(
{ name: req.body.tablename },
{ $addToSet : { guests: req.body.guestId } },
{ safe: true, upsert: true },
(err) => {
if (err) {
console.log(err);
} else {
// do stuff
}
},
);
res.send('back');
};
I am not sure if addToSet is the best solution because the query being executed twice.
If you used a callback and a promise simultaneously, it would make the query executes twice.
So choosing one of them would make it works fine.
Like below:
async updateField({ fieldName, shop_id, item }) {
return Shop.findByIdAndUpdate(
shop_id,
{ $push: { menuItems: item } },
{ upsert: true, new: true }
);
}

Set or create embedded document in Mongoose

I have a a schema as follows:
/**
* Answer Schema
*/
var AnswerSchema = new Schema({
answer: Number,
owner_id: Schema.Types.ObjectId
});
/**
* Poll Schema
*/
var PollSchema = new Schema({
question: { type: String, required: true, trim: true },
choices: [ { type: String, required: true, trim: true} ],
answers: [AnswerSchema]
});
How do I set an answer for a given poll_id and the owner_id of the person answering the poll?
The term "set or create" sort of opens this up a bit.
Essentially you will add (create) new items in arrays using the $push operator with .update(), or otherwise with a standard JavaScript array push once you have the current document after a .find().
But considering you might want to "update" an existing answer for a given owner then it's possibly a two step operation. If not then you just want the "update" using the $push.
PollSchema.update(
{ "_id": poll_id, "answers.owner_id": owner_id }
{ "$set":{ "answers.answer": value } },
function(err,numAffected) {
if (err)
throw err; // or whatever handling
// If this didn't modify anything then $push a document
if ( numAffected != 0 ) {
PollSchema.update(
{ "_id": poll_id },
{ "$push": {
"answers": {
"owner_id": owner_id,
"answer": value
}
}},
function(err, numAffected) {
// more things in here
}
);
}
);
Or again, if you do not care about having more than one answer per "owner" then just use the $push update.

Creating multiple documents in mongoose only if it does not currently exist

I was wondering if to create multiple documents in mongoose, but only if they do not exist currently? From the documentation, I've found the code below to create multiple documents, but just wondering how to ensure that it does not create a document if it currently exist?
In particular, if one document already exists, I would like the other documents that are not currently created to be created (rather than the entire create operation to fail).
From Documentation
var array = [{ type: 'jelly bean' }, { type: 'snickers' }];
Candy.create(array, function (err, jellybean, snickers) {
if (err) // ...
});
As noted in the documentation, the .create() method is a shortcut function for creating a new document for the given model and "saving" it to the collection. This actually works like the more formal .save() method but in shortcut form.
What you are describing though is more akin to the "upsert" behavior of the MongoDB .update() method. Which can also apply to its .findAndModify cousin or specifically in mongoose, the .findOneAndUpdate() method.
So with some sample code:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/nodetest');
var candySchema = new Schema({
type: String
});
var Candy = mongoose.model( "Candy", candySchema );
var array = [
{ type: 'jelly bean' },
{ type: 'snickers' },
{ type: 'mars' },
{ type: 'snickers' }
];
array.forEach(function(n) {
Candy.findOneAndUpdate( n, n, { upsert: true }, function(err,doc) {
console.log( doc );
});
});
You would see the following output:
{ _id: 535088e2e4beaab004e6cd97, type: 'jelly bean' }
{ _id: 535088e2e4beaab004e6cd98, type: 'snickers' }
{ _id: 535088e2e4beaab004e6cd99, type: 'mars' }
{ _id: 535088e2e4beaab004e6cd98, type: 'snickers' }
Noting that the second entry for 'snickers' actually refers to the object that was already created.
So that is a basic way to ensure that you are not actually creating the same data twice as long as you specify the "key" to match in the query condition.

How to select an element from a sub-documents array with two conditions?

Consider the following Mongoose schema:
new mongoose.Schema({
attributes: [{
key: { type: String, required: true },
references: [{
value: { type: String, required: true },
reference: { type: mongoose.Schema.Types.ObjectId, required: true }
}]
}
});
A document that follows this schema would look like this:
{
attributes: [
{
key: 'age', references: [{ value: '35', reference: 298387adef... }]
},
{
key: 'name', references: [{
value: 'Joe', reference: 13564afde...,
value: 'Joey', reference: 545675cdab...,
}
...
]
}
I'd like to select attributes according to the following conditions:
- the key is name for example
- the attribute with key name has a least one reference with a value Joe.
Ideally, I'd like to AND-chain many of these conditions. For example, {'name': 'Joe'} and {'age': '35'}.
I can't seem to find a way of doing that Mongoose. I've tried the following Mongoose queries without any good results (it gives either false positives or false negatives):
// First query
query.where('attributes.key', attribute.key);
query.where('attributes.references.value', attribute.value);
// Second
query.and([{ 'attributes.key': attribute.key }, { 'attributes.$.references.value': attribute.value }]);
// Third
query.where('attributes', { 'key': attribute.key, 'references.value': { $in: [attribute.value] }});
So how do I do it?
You can use elemMatch to find docs that contain an attributes element that matches multiple terms:
query.elemMatch(attributes, { key: 'name', 'references.value': 'Joe' })
You can't chain multiple elemMatch calls together though, so if you want to AND multiples of these you'd need to explicitly build up a query object using $and and $elemMatch instead of chaining Query method calls.