MongoDB/Mongoose aggregation or $or not working as expected? - node.js

Have some checkbox buttons in html that I would like to filter the results via the API on.click, over AJAX. There are 3 kinds of results: upVoted, noVote, downVoted. Subject to which of the checkboxes are checked, a combination of upVoted, noVote, and downVoted docs would appear to the user.
The ajax calls the URL of the API, and the data reaches the database call correctly after running through the following loop in the node.js API, which loops through the array of the strings ["upVoted", "noVote", "downVoted"] (or any combination thereof) and assigns the boolean value of true to the respective variable:
var upVoted = false;
var noVote = false;
var downVoted = false;
storyFiltering.forEach(storyFiltering);
function storyFiltering(element, index, array) {
if (element == 'upVoted') {upVoted = true}
else if (element == 'noVote') {noVote = true}
else if (element == 'downVoted') {downVoted = true}
}
console.log('upVoted: ' + upVoted + ', noVote: ' + noVote + ', downVoted: ' + downVoted);
These variables then match the Booleans of the voting in each of the documents.
Here's the database structure (with voting only one Boolean is ever true at once, the rest are false):
to: ["user1"],
voting: {
upVoted: true,
noVote: false,
downVoted: false
},
rank: 4
to: ["user2"],
voting: {
upVoted: true,
noVote: false,
downVoted: false
},
rank: 2
to: ["user1", "user2"],
voting: {
upVoted: true,
noVote: false,
downVoted: false
},
rank: 1
to: ["user1", "user2"],
voting: {
upVoted: false,
noVote: true,
downVoted: false
},
rank: 5
to: ["user1"],
voting: {
upVoted: false,
noVote: false,
downVoted: true
},
rank: 3
The result of the find (or aggregate) would be a callback of data filtered to match the toggled voting booleans..and then sorting them by descending 'rank'.
Toggling for all of the upVoted and noVote documents for user1 would return:
to: ["user1", "user2"],
voting: {
upVoted: true,
noVote: false,
downVoted: false
},
rank: 1
to: ["user1"],
voting: {
upVoted: true,
noVote: false,
downVoted: false
},
rank: 4
to: ["user1", "user2"],
voting: {
upVoted: false,
noVote: true,
downVoted: false
},
rank: 5
..whereas toggling for only the upVote documents for user1 would return:
to: ["user1", "user2"],
voting: {
upVoted: true,
noVote: false,
downVoted: false
},
rank: 1
to: ["user1"],
voting: {
upVoted: true,
noVote: false,
downVoted: false
},
rank: 4
..and in the same pattern, toggling for all of the upVoted + noVote + downVoted documents for User 1 would return all of the documents that include User 1, sorted by rank (1, 3, 4, 5)..while, constrastly, toggling for all of the downVoted documents for User 2 would return zero documents.

Here is your basic problem with your approach as I see it. What you are trying to do is have interaction with a user interface that "selects" certain items that you want to appear in your query results. That is all fine, but clearly you seem to be looking for "one query to rule them all" and this is where you come unstuck.
Taking your first example, your query for all "upvote" and "novote" documents comes out like this. Also stating away from aggregate right now, but the same applies as a query to a $match pipeline:
db.collection.find({
"to": { "$in": ["user1"] },
"$or": [
{ "voting.upVoted": true },
{ "voting.noVote": true }
]
})
That returns your expected results as the $or considers that "either" of those conditions could evaluate to true in order to satisfy the match. As explained before, all queries in MongoDB are implicitly an "and" query, so there is no need for the additional operator here.
In your next example, you really only need one of your criteria to be true, so we could remove the $or but I'll leave it here for the explanation later.
db.collection.find({
"to": { "$in": ["user1"] },
"$or": [
{ "voting.upVoted": true }
]
})
Again that matches your expected result as this returns both where the "user1" is present "and" that the "upVoted" value is true.
But now onto your interface interaction and how to handle that. Let us say that you have input coming from your interface that looks something like this:
{
"to": "user1",
"voting": {
"upVoted": true,
"downVoted": false,
"noVote": true
}
}
That confirms your basic selections in your user interface that were made for the query. In order to "process" this into your desired query then consider the following JavaScript code, to translate into whatever language you want to implement in. I'm just going to assume that structure as shown to be in a variable known as request:
var query = {
"to": { "$in": [] },
"$or": []
};
for ( k in request ) {
if ( k == "to" )
query.to["$in"].push( request.to );
if ( k == "voting" ) {
for ( v in request[k] ) {
if ( request[k][v] ) { // if that value is `true`
var obj = {};
obj[ k + "." + v] = true;
query["$or"].push( obj );
}
}
}
}
Running the code over that data the resulting query object now looks like:
{
"to" : {
"$in" : [
"user1"
]
},
"$or" : [
{
"voting.upVoted" : true
},
{
"voting.noVote" : true
}
]
}
Which is the same as the query we issued first, and changing your selections in request to look the way it would in the second example:
{
"to": "user1",
"voting": {
"upVoted": true,
"downVoted": false,
"noVote": false
}
}
The the resulting query object is:
{
"to" : {
"$in" : [
"user1"
]
},
"$or" : [
{
"voting.upVoted" : true
}
]
}
All that is left to do is issue the query which uses the object as it's argument:
db.collection.find( query )
So that is how you interact with data from your interface. You build dynamically, rather than try to throw all of the values you got in the response as parameters in the query.

Related

Mongoose - How to query for one of the fields in a list of objects, but update all of the fields?

I have a schema UserSettings that looks as follows:
{
"id": "5d8b987f9f8b9f9c8c8b9f9",
"settings": [
{
"setting_id": "112378482033233",
"is_active": true,
},
{
"setting_id": "115677743203553",
"is_active": true,
}
]
}
I want to be able to use mongoose to findOneAndUpdate one of the setting objects with a specific id to replace with a new one.
I currently have:
let setting_object = {
setting_id: settingId,
is_active: isActive,
}
return await models.UserSettings.findOneAndUpdate({
id: mongoose.Types.ObjectId(user.id)
},
{
$set: {
settings: setting_object
}
},
{
new: true,
upsert: true
});
The issue with this is that if is_active is the opposite boolean as the one in the document DB, it will add a new object in the settings array instead of replace with the one with the same id.
So if i had
let setting_object = {
setting_id: 112378482033233,
is_active: false,
}
and did the findOneAndUpdate above, the new DB would read:
{
"id": "5d8b987f9f8b9f9c8c8b9f9",
"settings": [
{
"setting_id": "112378482033233",
"is_active": true,
},
{
"setting_id": "112378482033233",
"is_active": false,
},
{
"setting_id": "115677743203553",
"is_active": true,
}
]
}
I read some things with using mongoose's aggregate but I haven't totally understood how this connects with my example. Thanks in advance!

Sequelize js reformat nested response

I am using PostgreSQL as database and Sequelize JS to Query my database. One of the APIs has below code
var video_attributes = [[sequelize.cast(sequelize.fn("like_count", Sequelize.col("video_id")), 'integer'), "total_likes"]];
if (request.user) {
video_attributes.push([sequelize.fn("has_liked", request.user.id, Sequelize.col("user_bookmark.video_id")), "is_liked"]);
video_attributes.push([sequelize.fn("has_bookmarked", request.user.id, Sequelize.col("user_bookmark.video_id")), "is_bookmarked"]);
}
mod_idx.user_bookmark.findAll({
where: {"user_id": request.user.id},
include : [{
model:mod_idx.video, as: "video",
attributes: {include: video_attributes}
}],
attributes: {exclude: ["user_id", "video_id", "id"]},
order: [[
"id",
"desc"
]],
}).then(video_list => {
let r = { "data": video_list, "success": true }
response.status(200).json(r)
}).catch(function (err) {
console.log(err)
});
It returns below response:
{
"data": [
{
"video": {
"id": 189,
"total_likes": 0,
"is_liked": false,
"is_bookmarked": true
}
},
{
"video": {
"id": 261,
"total_likes": 0,
"is_liked": false,
"is_bookmarked": true
}
}
],
"success": true
}
Expected result:
{
"data": [
{
"id": 189,
"total_likes": 0,
"is_liked": false,
"is_bookmarked": true
},
{
"id": 261,
"total_likes": 0,
"is_liked": false,
"is_bookmarked": true
}
],
"success": true
}
I tried by making "raw" as true but it returns column names as 'video.column_name' (https://stackoverflow.com/a/53612698/1030951).
I don't want to use map function as it may slowdown while processing large number of records.
It is not pretty but you can do this by adding all attributes at top level.
mod_idx.user_bookmark.findAll({
where: {"user_id": request.user.id},
include : [{
model:mod_idx.video, as: "video",
attributes: [] // Do not include anything in nested object
}],
attributes: [
[sequelize.cast(sequelize.fn("like_count", Sequelize.col("video.video_id")), 'integer'), "total_likes"]
// Add more here
],
order: [[
"id",
"desc"
]],
})
================================================================
Update
You can do something like this. One thing to consider here is that you must avoid the column name conflicts across multiple tables (as you are putting all attributes at top level), so add exclude or rename to make sure you handle the conflicts.
I demonstrate here for a scenario that I want to include all video attributes except certain columns to avoid conflicts.
const videoExcludeList = ['createdAt', 'updatedAt'];
// Get all video attributes minus exclude list.
const videoAttrs = Object.keys(mod_idx.video.rawAttributes)
.filter((v) => !videoExcludeList.includes(v))
.map((v) => Sequelize.col(`video.${v}`));
// Custom attributes that uses fn/rename/etc
const customAttrs = [
[sequelize.cast(sequelize.fn("like_count", Sequelize.col("video.video_id")), 'integer'), "total_likes"],
// Add more here.
]
You can also combine with top-level exclude to exclude columns from user_bookmark.
mod_idx.user_bookmark.findAll({
where: {"user_id": request.user.id},
include : [{
model:mod_idx.video, as: "video",
attributes: [] // Do not include anything in nested object
}],
attributes: {
exclude: ['id'], // exclude these columns from user_bookmark
include: [...videoAttrs, ...customAttrs],
},
order: [[
"id",
"desc"
]],
raw: true // You need this.
})

MongoDB: not all the results are returned from a query, using $geoNear

I got this query :
exports.search = (req, res) => {
let lat1 = req.body.lat;
let lon1 = req.body.lng;
let page = req.body.page || 1;
let perPage = req.body.perPage || 10;
let radius = req.body.radius || 100000; // This is not causing the issue, i can remove it and the issue is still here
var options = { page: page, limit: perPage, sortBy: { updatedDate: -1 } }
let match = {}
var aggregate = null;
if (lat1 && lon1) {
aggregate = Tutor.aggregate([
{
"$geoNear": {
"near": {
"type": "Point",
"coordinates": [lon1, lat1]
},
"distanceField": "distance", // this calculated distance will be compared in next section
"distanceMultiplier": 0.001,
"spherical": true,
"key": "loc",
"maxDistance": radius
}
},
{
$match: match
},
{ "$addFields": { "islt": { "$cond": [{ "$lt": ["$distance", "$range"] }, true, false] } } },
{ "$match": { "islt": true } },
{ "$project": { "islt": 0 } }
])
// .allowDiskUse(true);
} else {
aggregate = Tutor.aggregate([
{
$match: match
}
]);
}
Tutor
.aggregatePaginate(aggregate, options, function (err, result, pageCount, count) {
if (err) {
console.log(err)
return res.status(400).send(err);
}
else {
var opts = [
{ path: 'levels', select: 'name' },
{ path: 'subjects', select: 'name' },
{ path: 'assos', select: 'name' }
];
Tutor
.populate(result, opts)
.then(result2 => {
return res.send({
page: page,
perPage: perPage,
pageCount: pageCount,
documentCount: count,
tutors: result2
});
})
.catch(err => {
return res.status(400).send(err);
});
}
})
};
The query is supposed to retrieve all the tutors in a given range (which is a field from the tutor model, an integer in km, indicating how far the tutor is willing to move) around a certain location. (lat1, lon1).
The issue is that all the documents are not returned. After many tests, I have noticed that only tutors that are less than approximatively 7.5km away from the location are returned and not the others. Even if the tutor is 10km away and has a range of 15km, he won't be returned as he is farer than 7.5km.
I have tried switching location between two tutors (one that is returned and one that is not but should be) to see if this is the only thing causing the issue and it is. After I switched their location (lng and loc), the one that was returned before is no longer and vice versa.
I really don't get why this is happening.
Also, I know the result size is less than 16MB since I don't get all the results, even with allowDiskUse:true.
If you have any other idea about why I'm not getting all the results, don't hesitate !
Thank you !
PS : this is a part of the tutor model with the concerned fields (loc):
import mongoose from 'mongoose';
import validate from 'mongoose-validator';
import { User } from './user';
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate';
var ObjectId = mongoose.Schema.Types.ObjectId;
var rangeValidator = [
validate({
validator: (v) => {
v.isInteger && v >= 0 && v <= 100;
},
message: '{VALUE} is a wrong value for range'
})
];
var tutorSchema = mongoose.Schema({
fullName: {
type: String,
trim: true,
minlength: [1, 'Full name can not be empty'],
required: [true, 'Full name is required']
},
location: {
address_components: [
{
long_name: String,
short_name: String,
types: String
}
],
description: String,
lat: Number,
lng: Number
},
loc: {
type: { type: String },
coordinates: []
},
});
tutorSchema.plugin(mongooseAggregatePaginate);
tutorSchema.index({ "loc": "2dsphere" });
var Tutor = User.discriminator('Tutor', tutorSchema);
module.exports = {
Tutor
};
The user model is using two indexes. The ID and this one ;
db['system.indexes'].find() Raw Output
{
"v": 2,
"key": {
"loc": "2dsphere"
},
"name": "loc_2dsphere",
"background": true,
"2dsphereIndexVersion": 3,
"ns": "verygoodprof.users"
}
I also have some similar kind of problem, in my case there is problem with limit
https://docs.mongodb.com/manual/reference/operator/aggregation/geoNear/
by default limit is 100 (Optional. The maximum number of documents to return. The default value is 100).
If you want you can increase the limit. Hope it help
You are using spherical: true, having the semantics different from planar geospacial queries.
Also, since you are using a GeoJSON object, instead of plain coordinates, the distance MUST be provided in meters (or in kilometers, since you are using the .001 multiplier)
When using spherical: true, it uses the $nearSphere query: https://docs.mongodb.com/manual/reference/operator/query/nearSphere/#op._S_nearSphere
With spherical: false, mongo uses the $near query: https://docs.mongodb.com/manual/reference/operator/query/near/#op._S_near
Since you are working with Lat/Lng, meaning planar coordinates, you should disable the spherical option.
The distance returned by $geoNear in case of "spherical": true is in radians so you need to change the "distanceMultiplier": 6371 to be equal to earth radius to get the distance in Km.

How to create item if not exists and return an error if exists

I'm writing alexa skill and would like to check if user exists in MongoDB. My code works but I don't know how to define situation if user is already in a database :(
Everytime when I execute code I get:
"Hello Anna you are new here"
My user Anna is saved in MongoDB
But I would like to distinguish when my user is already in a database and react for that.
Does anybody smart has a solution for my problem?
var myName = "Anan1";
var userID = this.event.session.user.userId;
console.log(userID);
self = this;
User.findOneAndUpdate(
{userId: userID},
{$set:{name:myName}},
{upsert: true, new: false, runValidators: true},
function(err, doc){
if(err){
console.log("eeoror");
}
console.log(doc);
if (doc==null){
self.emit(':ask',
"Hello "+ myName +"you are new here")
}else {
self.emit(':ask',
"Hello "+ myName +"you are not new here")
}
});
It sounds like what you really want is a unique key constraint and not an upsert.
The unique key can be set in [mongoose] with either the schema field options:
const s = new Schema({ name: { type: String, unique: true }});
or by the index method:
Schema.path('name').index({ unique: true });
If an attempt is made to create a document that already has an entry for that key then an error will be thrown:
NOTE: violating the constraint returns an E11000 error from MongoDB when saving, not a Mongoose validation error.
As noted in comment earlier, you have two basic approaches to work out whether something was "created" or not. These are either to:
Return the rawResult in the response and check the updatedExisting property which tells you if it's an "upsert" or not
Set new: false so that "no document" is actually returned in result when it's actually an "upsert"
As a listing to demonstrate:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/thereornot';
mongoose.set('debug', true);
mongoose.Promise = global.Promise;
const userSchema = new Schema({
username: { type: String, unique: true }, // Just to prove a point really
password: String
});
const User = mongoose.model('User', userSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri);
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
// Shows updatedExisting as false - Therefore "created"
let bill1 = await User.findOneAndUpdate(
{ username: 'Bill' },
{ $setOnInsert: { password: 'password' } },
{ upsert: true, new: true, rawResult: true }
);
log(bill1);
// Shows updatedExisting as true - Therefore "existing"
let bill2 = await User.findOneAndUpdate(
{ username: 'Bill' },
{ $setOnInsert: { password: 'password' } },
{ upsert: true, new: true, rawResult: true }
);
log(bill2);
// Test with something like:
// if ( bill2.lastErrorObject.updatedExisting ) throw new Error("already there");
// Return will be null on "created"
let ted1 = await User.findOneAndUpdate(
{ username: 'Ted' },
{ $setOnInsert: { password: 'password' } },
{ upsert: true, new: false }
);
log(ted1);
// Return will be an object where "existing" and found
let ted2 = await User.findOneAndUpdate(
{ username: 'Ted' },
{ $setOnInsert: { password: 'password' } },
{ upsert: true, new: false }
);
log(ted2);
// Test with something like:
// if (ted2 !== null) throw new Error("already there");
// Demonstrating "why" we reserve the "Duplicate" error
let fred1 = await User.findOneAndUpdate(
{ username: 'Fred', password: 'password' },
{ $setOnInsert: { } },
{ upsert: true, new: false }
);
log(fred1); // null - so okay
let fred2 = await User.findOneAndUpdate(
{ username: 'Fred', password: 'badpassword' }, // <-- dup key for wrong password
{ $setOnInsert: { } },
{ upsert: true, new: false }
);
mongoose.disconnect();
} catch(e) {
console.error(e)
} finally {
process.exit()
}
})()
And the output:
Mongoose: users.remove({}, {})
Mongoose: users.findAndModify({ username: 'Bill' }, [], { '$setOnInsert': { password: 'password', __v: 0 } }, { upsert: true, new: true, rawResult: true, remove: false, fields: {} })
{
"lastErrorObject": {
"n": 1,
"updatedExisting": false,
"upserted": "5adfc8696878cfc4992e7634"
},
"value": {
"_id": "5adfc8696878cfc4992e7634",
"username": "Bill",
"__v": 0,
"password": "password"
},
"ok": 1,
"operationTime": "6548172736517111811",
"$clusterTime": {
"clusterTime": "6548172736517111811",
"signature": {
"hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"keyId": 0
}
}
}
Mongoose: users.findAndModify({ username: 'Bill' }, [], { '$setOnInsert': { password: 'password', __v: 0 } }, { upsert: true, new: true, rawResult: true, remove: false, fields: {} })
{
"lastErrorObject": {
"n": 1,
"updatedExisting": true
},
"value": {
"_id": "5adfc8696878cfc4992e7634",
"username": "Bill",
"__v": 0,
"password": "password"
},
"ok": 1,
"operationTime": "6548172736517111811",
"$clusterTime": {
"clusterTime": "6548172736517111811",
"signature": {
"hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"keyId": 0
}
}
}
Mongoose: users.findAndModify({ username: 'Ted' }, [], { '$setOnInsert': { password: 'password', __v: 0 } }, { upsert: true, new: false, remove: false, fields: {} })
null
Mongoose: users.findAndModify({ username: 'Ted' }, [], { '$setOnInsert': { password: 'password', __v: 0 } }, { upsert: true, new: false, remove: false, fields: {} })
{
"_id": "5adfc8696878cfc4992e7639",
"username": "Ted",
"__v": 0,
"password": "password"
}
So the first case actually considers this code:
User.findOneAndUpdate(
{ username: 'Bill' },
{ $setOnInsert: { password: 'password' } },
{ upsert: true, new: true, rawResult: true }
)
Most options are standard here as "all" "upsert" actions will result in the field content being used to "match" ( i.e the username ) is "always" created in the new document, so you don't need to $set that field. In order to not actually "modify" other fields on subsequent requests you can use $setOnInsert, which only adds these properties during an "upsert" action where no match is found.
Here the standard new: true is used to return the "modified" document from the action, but the difference is in the rawResult as is shown in the returned response:
{
"lastErrorObject": {
"n": 1,
"updatedExisting": false,
"upserted": "5adfc8696878cfc4992e7634"
},
"value": {
"_id": "5adfc8696878cfc4992e7634",
"username": "Bill",
"__v": 0,
"password": "password"
},
"ok": 1,
"operationTime": "6548172736517111811",
"$clusterTime": {
"clusterTime": "6548172736517111811",
"signature": {
"hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"keyId": 0
}
}
}
Instead of a "mongoose document" you get the actual "raw" response from the driver. The actual document content is under the "value" property, but it's the "lastErrorObject" we are interested in.
Here we see the property updatedExisting: false. This indicates that "no match" was actually found, thus a new document was "created". So you can use this to determine that creation actually happened.
When you issue the same query options again, the result will be different:
{
"lastErrorObject": {
"n": 1,
"updatedExisting": true // <--- Now I'm true
},
"value": {
"_id": "5adfc8696878cfc4992e7634",
"username": "Bill",
"__v": 0,
"password": "password"
},
"ok": 1,
"operationTime": "6548172736517111811",
"$clusterTime": {
"clusterTime": "6548172736517111811",
"signature": {
"hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"keyId": 0
}
}
}
The updatedExisting value is now true, and this is because there already was a document that matched the username: 'Bill' in the query statement. This tells you the document was already there, so you can then branch your logic to return an "Error" or whatever response you want.
In the other case, it may be desirable to "not" return the "raw" response and use a returned "mongoose document" instead. In this case we vary the value to be new: false without the rawResult option.
User.findOneAndUpdate(
{ username: 'Ted' },
{ $setOnInsert: { password: 'password' } },
{ upsert: true, new: false }
)
Most of the same things apply except that now the action is the original state of the document is returned as opposed to the "modified" state of the document "after" the action. Therefore when there is no document that actually matches the "query" statement, the returned result is null:
Mongoose: users.findAndModify({ username: 'Ted' }, [], { '$setOnInsert': { password: 'password', __v: 0 } }, { upsert: true, new: false, remove: false, fields: {} })
null // <-- Got null in response :(
This tells you the document was "created", and it's arguable that you already know what the content of the document should be since you sent that data with the statement ( ideally in the $setOnInsert ). Point being, you already know what to return "should" you require to actually return the document content.
By contrast, a "found" document returns the "original state" showing the document "before" it was modified:
{
"_id": "5adfc8696878cfc4992e7639",
"username": "Ted",
"__v": 0,
"password": "password"
}
Therefore any response which is "not null" is therefore an indication that the document was already present, and again you can branch your logic depending on what was actually received in response.
So those are the two basic approaches to what you are asking, and they most certainly "do work"! And just as is demonstrated and reproducible with the same statements here.
Addendum - Reserve Duplicate Key for bad passwords
There is one more valid approach that is hinted at in the full listing as well, which is essentially to simply .insert() ( or .create() from mongoose models ) new data and have a "duplicate key" error throw where the "unique" property by index is actually encountered. It's a valid approach but there is one particular use case in "user validation" which is a handy piece of logic handling, and that is "validating passwords".
So it's a pretty common pattern to retrieve user information by the username and password combination. In the case of an "upsert" this combination justifies as "unique" and therefore an "insert" is attempted if no match is found. This is exactly what makes matching the password a useful implementation here.
Consider the following:
// Demonstrating "why" we reserve the "Duplicate" error
let fred1 = await User.findOneAndUpdate(
{ username: 'Fred', password: 'password' },
{ $setOnInsert: { } },
{ upsert: true, new: false }
);
log(fred1); // null - so okay
let fred2 = await User.findOneAndUpdate(
{ username: 'Fred', password: 'badpassword' }, // <-- dup key for wrong password
{ $setOnInsert: { } },
{ upsert: true, new: false }
);
On the first attempt we don't actually have a username for "Fred", so the "upsert" would occur and all the other things as already described above happen to identify whether it was a creation or a found document.
The statement that follows uses the same username value but provides a different password to what is recorded. Here MongoDB attempts to "create" the new document since it did not match on the combination, but because the username is expected to be "unique" you receive a "Duplicate key error":
{ MongoError: E11000 duplicate key error collection: thereornot.users index: username_1 dup key: { : "Fred" }
So what you should realize is you now get three conditions to evaluate for "free". Being:
The "upsert" was recorded by either the updatedExisting: false or null result depending on the method.
You know the document ( by combination ) "exists" via either the updatedExisting: true or where the document returns was "not null".
If the password provided was not a match for what already existed for the username, then you would get the "duplicate key error" which you can trap and respond accordingly, advising the user in response that the "password is incorrect".
All of that from one request.
That's the main reasoning for using "upserts" as opposed to simply throwing inserts at a collection, as you can get different branching of the logic without making additional requests to the database to determine "which" of those conditions should be the actual response.

Mongoose .dot Notation equivalent for $and + $in + $or?

Am looking to '.find' all docs, '.where' the username is '.in' the to: array and either upVoted: true '.or' noVote: true, all sorted by rank descending
Here's an example doc structure:
to: [String],
voting: {
upVoted: Boolean,
noVote: Boolean,
downVoted: Boolean
},
rank: Number
This query is working, but how would this be written in Mongoose .dot notation:
Story.find({
$and: [
{ to: { $in: [ 'user1' ] } },
{ $or: [{ 'voting.upVote': true }, { 'voting.noVote': true }] }
]
}, function (err, stories) {
FYI am working on the correct syntax for sorting this
Your original query can be made simpler, because you don't need $and (it's implicit in MongoDB). Also, if you are looking for only one user from the ṫo array, then you also don't need the $in operator.
{
to: 'user1',
$or: [{ 'voting.upVote': true }, { 'voting.noVote': true }]
}
Using Mongoose query API:
var query = Story.find();
query.where('to', 'user1');
query.or({ 'voting.upVote': true }, { 'voting.noVote': true });
query.exec(function (err, doc) {
if (err) ...
});
Or if you're looking for more than one user then replace:
query.where('to', 'user1');
with:
query.where('to').in(['user1', 'user2']);

Resources