Return document with all array of objects where element exists - mongodb

I'm trying to return one document with an array of objects where userAnswer exists. My query below returns only the first object with the array. What am I missing here? It should return two.
Meteor and MongoDB being used.
MongoDB: document
{
_id: 1,
questions: [
{ question: 'test question', userAnswer: 'answer' },
{ question: 'test question two', userAnswer: 'answertwo' },
{ question: 'test question three' }
]
};
Mongodb: Query
ConductedExams.findOne(
{
userId,
examId,
userCompletedExam: null
},
{ fields: { questions: { $elemMatch: { userAnswer: { $exists: true } } } } }
);

"findOne" return only one element.
You should use "find".

Related

Mongoose findOneAndUpdate with $addToSet pushes duplicate

I have a schema such as
listSchema = new Schema({
...,
arts: [
{
...,
art: { type: Schema.Types.ObjectId, ref: 'Art', required: true },
note: Number
}
]
})
My goal is to find this document, push an object but without duplicate
The object look like
var art = { art: req.body.art, note: req.body.note }
The code I tried to use is
List.findOneAndUpdate({ _id: listId, user: req.myUser._id },
{ $addToSet: { arts: art} },
(err, list) => {
if (err) {
console.error(err);
return res.status(400).send()
} else {
if (list) {
console.log(list)
return res.status(200).json(list)
} else {
return res.status(404).send()
}
}
})
And yet there are multiple entries with the same Art id in my Arts array.
Also, the documentation isn't clear at all on which method to use to update something. Is this the correct way ? Or should I retrieve and then modify my object and .save() it ?
Found a recent link that came from this
List.findOneAndUpdate({ _id: listId, user: req.user._id, 'arts.art': artId }, { $set: { 'arts.$[elem]': artEntry } }, { arrayFilters: [{ 'elem.art': mongoose.Types.ObjectId(artId) }] })
artworkEntry being my modifications/push.
But the more I'm using Mongoose, the more it feels they want you to use .save() and modify the entries yourself using direct modification.
This might cause some concurrency but they introduced recently a, option to use on the schema { optimisticConcurrency: true } which might solve this problem.

How to project updated values only using findOneAndUpdate in embedded array Mongoose?

Currently my User model looks like:
{
_id: 'SomeId'
firstName: 'John',
lastName: 'Cena',
books: [
{
_id: 'xyz',
title: 'a',
author:'b',
ratings:[
{source:'source1', value:"8"},
{source:'source2', value:"9"}]
},
{
_id: 'abc',
title: 'c',
author:'d',
ratings:[
{source:'source3', value:"7"},
{source:'source4', value:"5"}]
}
]
}
After making an findOneAndUpdate query to update rating=>value of 1st book object(_id: "xyz") from 8 to 10 for a given source(say "source1"):
let up={
'books.$[book].ratings.$[rating].value':10
}
let filter={
new:true,
'books.rating':1, //I just want rating array of updated objects in it
arrayFilters:[
{ 'book._id':'xyz'},
{ 'rating.source': 'source1'}
]
}
User.findOneAndUpdate({'_id':'userId','books._id':'xyz'},up,filter).select('books.rating').exec((err,doc)=> {
if (err) throw err;
console.log(doc);
}
My code updates the books=>rating=>value correctly but I can't get that updated rating of that book.
This gives me rating of all books with both updated and non updated values in it. Looks like:-
{
books: [{ ratings:[{source:'source1', value:"10"},{source:'source2', value:"9"}] },
{ ratings:[{source:'source3', value:"7"},{source:'source4', value:"5"}] }]
}
I think the data of 2nd book shouldn't be there at all according to my code. I expect the follwing output:
{
books: [{ ratings:[{source:'source1', value:"10"}] }
}
Please help me to write findOneAndUpdate query correctly!
you can use array.find() like this:
const updatebookSource = (sourceId, userId, bookId) => {
User.findOneAndUpdate({ _id: userId, "books._id": bookId }, up, filter).exec(
(err, doc) => {
if (err) throw err;
let res = doc.books[0].ratings.find(rating => {
return rating.source === sourceId;
});
console.log(JSON.stringify(res, null, 1));
}
);
};
This returns the updated object. Let me know if it works.

Mongoose query using if else possible?

I have this Schema:
const guestSchema = new Schema({
id: String,
cart: [
{
product: {
type: mongoose.Schema.ObjectId,
ref: "products"
},
quantity: Number
}
]
});
I have this query:
Guest.findOneAndUpdate(
{ id: req.sessionID },
{
$cond: [
{ "cart.product": { $ne: req.body.itemID } },
{ $push: { "cart": { product: req.body.itemID, quantity: 1 } } },
{ $inc: { "cart.quantity": 1 } }
]
},
{ upsert: true, new: true }
).exec(function(err, docs) {
err ? console.log(err) : res.send(docs);
});
Basically, what I'm trying to do is update based on a condition. I tried using $cond, but found out that operator isn't used for querys like I'm doing.
Based on this:
{ $cond: [ <boolean-expression>, <true-case>, <false-case> ] }
I want something similar to the functionality of this operator for my query.
Let's break down my condition:
For my boolean expression: I want to check if req.body.itemID is $ne to any of the values in my cart
If true then: $push the itemID and quantity into the cart
Else (then item already exists): $inc the quantity by 1
Question: How would achieve this result? Do I need to make two seperate querys? I'm trying to avoid doing that if possible
I went through all their Update Field Operators, and there's probably no way to do this in the way I want.
I wonder why there is no $cond for update operators. Nonetheless, I have the solution to what I wanted the functionality accomplish. Just not in the elegant fashion that I would like it.
Guest.findOneAndUpdate(
{ id: req.sessionID },
{ id: req.sessionID }, //This is here in case need to upsert new guest
{ upsert: true, new: true }
).exec(function(err, docs) {
if (err) {
console.log(err);
} else {
//Find the index of the item in my cart
//Returns (-1) if not found
const item = doc.cart.findIndex(
item => item.product == req.body.itemID
);
if (item !== -1) {
//Item found, so increment quantity by 1
doc.cart[item].quantity += 1;
} else {
//Item not found, so push into cart array
doc.cart.push({ product: req.body.itemID, quantity: 1 });
}
doc.save();
}
});
This type of logic does not belong within the database query. It should happen in the application layer. MongoDB is also very fast at retrieving and updating single records with an index so that should not be a concern.
Please try doing something like this:
try {
const guest = await Guest.findOne().where({
id: req.sessionID
}).exec();
// your cond logic, and update the object
await guest.save();
res.status(200).json(guest);
} catch (error) {
handleError(res, error.message);
}

Can I access the positional $ operator in projection of findOneAndUpdate

I have this query that works, but I want for the doc to only display network.stations.$ instead of the entire array. If I write fields: network.stations.$, I get an error. Is there a way for the doc only to return a single element from [stations]?
Network.findOneAndUpdate({
"network.stations.id": req.params.station_Id
}, {
"network.stations.$.free_bikes": req.body.free_bikes
}, {
new: true,
fields: "network.stations"
}, (err, doc) => console.log(doc))
// I want doc to somehow point only to a single station instead of
// several stations like it currently does.
The answer is "yes", but not in the way you are expecting. As you note in the question, putting network.stations.$ in the "fields" option to positionally return the "modified" document throws a specific error:
"cannot use a positional projection and return the new document"
This however should be the "hint", because you don't really "need" the "new document" when you know what the value was you are modifying. The simple case then is to not return the "new" document, but instead return it's "found state" which was "before the atomic modification" and simply make the same modification to the returned data as you asked to apply in the statement.
As a small contained demo:
const mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
const uri = 'mongodb://localhost/test',
options = { useMongoClient: true };
const testSchema = new Schema({},{ strict: false });
const Test = mongoose.model('Test', testSchema, 'collection');
function log(data) {
console.log(JSON.stringify(data,undefined,2))
}
(async function() {
try {
const conn = await mongoose.connect(uri,options);
await Test.remove();
await Test.insertMany([{ a: [{ b: 1 }, { b: 2 }] }]);
for ( let i of [1,2] ) {
let result = await Test.findOneAndUpdate(
{ "a.b": { "$gte": 2 } },
{ "$inc": { "a.$.b": 1 } },
{ "fields": { "a.$": 1 } }
).lean();
console.log('returned');
log(result);
result.a[0].b = result.a[0].b + 1;
console.log('modified');
log(result);
}
} catch(e) {
console.error(e)
} finally {
mongoose.disconnect()
}
})();
Which produces:
Mongoose: collection.remove({}, {})
Mongoose: collection.insertMany([ { __v: 0, a: [ { b: 1 }, { b: 2 } ], _id: 59af214b6fb3533d274928c9 } ])
Mongoose: collection.findAndModify({ 'a.b': { '$gte': 2 } }, [], { '$inc': { 'a.$.b': 1 } }, { new: false, upsert: false, fields: { 'a.$': 1 } })
returned
{
"_id": "59af214b6fb3533d274928c9",
"a": [
{
"b": 2
}
]
}
modified
{
"_id": "59af214b6fb3533d274928c9",
"a": [
{
"b": 3
}
]
}
Mongoose: collection.findAndModify({ 'a.b': { '$gte': 2 } }, [], { '$inc': { 'a.$.b': 1 } }, { new: false, upsert: false, fields: { 'a.$': 1 } })
returned
{
"_id": "59af214b6fb3533d274928c9",
"a": [
{
"b": 3
}
]
}
modified
{
"_id": "59af214b6fb3533d274928c9",
"a": [
{
"b": 4
}
]
}
So I'm doing the modifications in a loop so you can see that the update is actually applied on the server as the next iteration increments the already incremented value.
Merely by omitting the "new" option, what you get is the document in the state which it was "matched" and it then is perfectly valid to return that document state before modification. The modification still happens.
All you need to do here is in turn make the same modification in code. Adding .lean() makes this simple, and again it's perfectly valid since you "know what you asked the server to do".
This is better than a separate query because "separately" the document can be modified by a different update in between your modification and the query to return just a projected matched field.
And it's better than returning "all" the elements and filtering later, because the potential could be a "very large array" when all you really want is the "matched element". Which of course this actually does.
Try changing fields to projection and then use the network.stations.$ like you tried before.
If your query is otherwise working then that might be enough. If it's still not working you can try changing the second argument to explicitly $set.
Network.findOneAndUpdate({
"network.stations.id": req.params.station_Id
}, {
"$set": {
"network.stations.$.free_bikes": req.body.free_bikes
}
}, {
new: true,
projection: "network.stations.$"
}, (err, doc) => console.log(doc))

Validate embedded document in mongodb

I'd like to validate my documents in MongoDB. As described here, the validator query can be specified during the createCollection command.
My application is a simple todo list, so the collection list has many documents like this:
{
"name": "the list name",
"color: "red",
"items": [
{
"title": "item1",
"isDone": false,
"text": "the item description"
}
]
}
How can I sure that all documents has this shape?
EDIT: I'm using MongoDB 3.4
You can't.
There's no way to avoid new property. If this needed is required, you should consider to migrate a SQL solution.
Otherwise, your document will contain at least those members and those members will be validated.
I assume that all fields are mandatory, so no list can be added neither without a color nor having an item without a title.
I assume that color is an enum also.
{
validator: {
name: { $type: 'string', $exists: true },
color: { $in: ['red', 'green', 'blue'], $exists: true },
items: { $exists: true },
$or: [
{ items: { $size: 0 } },
{
$and: [
{ 'items.0': { $exists: true } },
{
items: {
$not: {
$elemMatch: {
$not: { $type: 'object' }
}
}
}
},
{
items: {
$not: {
$elemMatch: {
$or: [
{ title: { $not: { $type: 'string' } } },
{ title: { $not: { $exists: true } } },
{ isDone: { $not: { $type: 'bool' } } },
{ isDone: { $not: { $exists: true } } },
{ text: { $not: { $type: 'string' } } },
{ text: { $not: { $exists: true } } }
]
}
}
}
}
]
}
]
}
}
Explanation
The solution is based on the second-order logic.
Indeed, the assertion "all elements have to match A" is equal to "no element should match !A" or, if you have many queries, "all element have to match A and B" becomes "no element should match !A or !B", where ! means the negation of the following predicate.
Long explanation
The first two query items assert that all documents have name and color, the third one the items property always exists.
NB: in the third query property, there's not the $type check because mongodb treats the array type in a odd way.
The $or operation below is needed because the item property can be empty or filled with some data.
First clause of $or checks if the array is empty.
If the array is not empty, three checks should be done:
the array contains at least one element
the array has to contain only object
all object has a specified shape.
So the first $and operator element checks that at least one element is present. The second one checks that all array elements are objects. The third one asserts that all objects have a specified shape.
In mongodb, there's an operator for checking if all array elements match a query. So, using the second order logic, the check should be turned over.
In fact, the last two clauses of $and check that no elements match none of the described query. Each sub query is the negate of a wanted query.
Example
fails
{ name: 'foo' }
{ color: 'red' }
{ color: 'unknown color' }
{ name: 'foo', color: 'red' }
{ name: 'foo', color: 'red', item: 3 }
{ name: 'foo', color: 'red', item: [ 3 ] }
{ name: 'foo', color: 'red', item: [ { } ] }
{ name: 'foo', color: 'red', item: [ { title: 'ww' } ] }
{ name: 'foo', color: 'red', item: [ { title: 'ww', isDone: false } ] }
{ name: 'foo', color: 'red', item: [ { title: 44, isDone: false, text: 'the text' } ] }
pass
{ name: 'foo', color: 'red', items: [ ] },
{ name: 'foo', color: 'red', items: [ ] },
{ name: 'foo', color: 'red', items: [ { title: 'the title', isDone: false, text: 'the text' }, { title: 'the title1', isDone: true, text: 'the text' } ] }
This solution is still valid using MongoDB 3.2