How to update with mongoose - mongodb

I have this record
{
"_id" : ObjectId("5dfdff479ad032cbbc673507"),
"selection" : [
{
"highlights" : "test",
"comment" : "CHANGE THIS",
"el" : "body:nth-child(2)>div:nth-child(2)#root>div.App>p:nth-child(1)"
},
{
"highlights" : "Barrett’s lyrical prose opens with a clever and tender solution",
"comment" : "",
"el" : "body:nth-child(2)>div:nth-child(2)#root>div.App>p:nth-child(2)"
}
],
"category" : [],
"status" : "",
"url" : "http://localhost:3000/theone",
"title" : "React App test",
"__v" : 4
}
And I want to update the comment. I have tried to use update and findOneAndUpdate and nothing is working. Here is my attempt
WebHighlight.findOneAndUpdate(
{
_id: req.params.highlight,
"selection.highlights": "test"
},
{ "selection.$.comment": "yourValue" }
);
That req.params.highlight is the id (I even hardcoded it)
I also tried this
WebHighlight.findById(req.params.highlight, (err, book) => {
var test = [...book.selection];
test[0].comment = "somethibf"
book.save();
res.json(book);
});
And nothing is working.
This is the model
const webhighlightsModel = new Schema({
selection: { type: Array, default: "" },
category: { type: Array, default: [] },
title: { type: String },
url: { type: String },
status: { type: String, default: "" }
});

Actually your code seems to work, but findOneAndUpdate returns the old document if you don't give {new: true} option.
I think for this reason, you think the update wasn't successfull, but if you check your collection, you will see the update.
WebHighlight.findOneAndUpdate(
{
_id: req.params.highlight,
"selection.highlights": "test"
},
{ "selection.$.comment": "yourValue" },
{ new: true }
)
.then(doc => res.send(doc))
.catch(err => res.status(500).send(err));
Also I think it would be better if selection had a sub schema like this:
const mongoose = require("mongoose");
const schema = new mongoose.Schema({
selection: [
new mongoose.Schema({
highlights: String,
comment: String,
el: String
})
],
category: { type: Array, default: [] },
title: { type: String },
url: { type: String },
status: { type: String, default: "" }
});
module.exports = mongoose.model("WebHighlight", schema);
So with this every selection would an _id field, and it would be better to update with this _id.

You should use the $set operator to update existing values:
WebHighlight.findOneAndUpdate(
{
_id: req.params.highlight,
"selection.highlights": "test"
},
{ '$set': { "selection.$.comment": "yourValue" } }
);

Related

Look up and create or update object inside array

I am currently trying to setup a schema for custom Discord guild commands:
const GuildCommandsSchema = new mongoose.Schema({
_id: String,
commands: [
{
name: {
type: String,
unique: true,
required: true,
},
action: {
type: String,
required: true,
},
author: {
type: String,
required: true,
},
},
],
});
Is this ok, performancewise, or could I improve it?
I feel like Mongo would need to look through all commands, since it can't index any commands inside 'commands' even though 'name' is unique.
If that's fine, how can I access the values inside commands?
I would need to find the right command via 'name' if it exists, otherwise create it and add/update 'action' + 'author'.
I tried something like this:
const updatedCommand = await GuildCommands.findOneAndUpdate(
{ _id },
{
$set: {
[`commands.$[outer].name`]: name,
[`commands.$[outer].action`]: action,
[`commands.$[outer].author`]: author,
},
},
{
arrayFilters: [{ 'outer.name': name }],
}
);
Unfortunately that does not create commands if they don't exist.
Thanks for your help
aggregate
db.collection.update({},
{
$set: {
"commands.$[c].name": "1",
"commands.$[c].author": "1",
"commands.$[c].action": "1"
}
},
{
arrayFilters: [
{
"c.author": "34"
}
],
multi: true
})
mongoplayground
To answer my own question:
I changed my Schema to use Maps instead of Arrays for performance improvments and also better model management.
const GuildCommandsSchema = new mongoose.Schema(
{
_id: String,
commands: {
type: Map,
of: {
_id: false,
name: {
type: String,
required: true,
},
action: {
type: String,
required: true,
},
active: {
type: Boolean,
required: true,
default: true,
},
author: {
type: String,
required: true,
},
},
},
},
{ versionKey: false }
);
The new query to find and update/create a command is also better imo:
const findCommand = await GuildCommands.findOne({ _id });
if (!action) {
const getCommand = findCommand.commands.get(name);
if (getCommand) {
message.reply(getCommand.action);
} else {
message.reply(`Cannot find ${name}`);
}
} else {
findCommand.commands.set(name, {
name,
action,
author,
});
findCommand.save();
}

MongoDB Mongoose find using Map

I have a mongoose schema like this:
const userSchema = new Schema( {
email:{ type: String, required: true, unique:true, index:true },
mobile:{ type: String },
phone:{ type: String },
firstname: { type: String },
lastname: { type: String },
profile_pic:{type:String},
socialHandles: {
type: Map,
of: String
},
active: {type:Boolean, default:true}
}, {
timestamps: true
} );
I want to query "give me user where socialHandles.instagram=jondoe" how do I do it?
Please help
Mongoose's map becomes a nested object in your database
{ "_id" : ObjectId(""), "socialHandles" : { "instagram": "jondoe" }, ..., "__v" : 0 }
so you can query it by using the dot notation:
let user = await User.findOne({ 'socialHandles.instagram': 'jondoe' });

Mongoose query on subdocument returns array of other subdocument using projection

Update: I am looking for an answer that works within mongodb projection: https://docs.mongodb.com/manual/reference/method/db.collection.findOne/#definition
I am trying to filter a query on a subdocument using projection so that it only returns a specific array. But when filtering the result also includes an array of another subdocument. When I don't filter it only returns the found document.
I tried different filtering options including and excluding positional elements, but can't get the desired return.
Mongoose schema
const stationSchema = new mongoose.Schema({
mac: String,
stationName: String,
syncReadings: Boolean,
temperature: Array,
humidity: Array,
measures: [{
date: Date,
temperature: Number,
humidity: Number
}],
lastUpdated: Date
});
// Define user schema
var userSchema = mongoose.Schema({
local : {
email : String,
password : String
},
facebook : {
id : String,
token : String,
name : String,
email : String
},
twitter : {
id : String,
token : String,
displayName : String,
username : String
},
google : {
id : String,
token : String,
email : String,
name : String
},
apiKey: String,
stations : [stationSchema]
},
{
usePushEach: true
}
);
Api handler
app.get('/api/stations/:stationName/measures', function(req, res, next) {
var user = {
apiKey: req.user.apiKey
}
const query = {
apiKey: user.apiKey,
stations.stationName': req.params.stationName
}
const options = {
'stations.measures': 1
}
User.findOne(query, options)
.exec()
.then(stations => {
res.status(200).send(stations)
})
.catch(err => {
console.log(err);
res.status(400).send(err);
})
});
There are two stations under one user:
[
{
"_id": "5c39c99356bbf002fb092ce9",
"stations": [
{
"stationName": "livingroom",
"mac": "5C:CF:7F:77:12:FB",
"_id": "5c39c9ab56bbf002fb092cea",
"lastUpdated": "2019-01-12T11:07:01.802Z",
"syncReadings": false,
"measures": [],
"humidity": [],
"temperature": [
{
"date": "2019-01-12T11:07:01.802Z",
"temperature": "20"
}
]
},
{
"stationName": "office",
"mac": "5C:CF:7F:77:12:FC",
"_id": "5c39cacdce4ac903123f0150",
"measures": [],
"humidity": [],
"temperature": []
}
]
}
]
API call
http://localhost:8080/api/stations/livingroom/measures
Result
{
"_id": "5c39c99356bbf002fb092ce9",
"stations": [
{
"measures": []
},
{
"measures": []
}
]
}
Projection options tried
const options = {
'stations.measures': 1
}
const options = {
'stations.$.measures': 1
}
const options = {
'stations.$': 1,
'stations.$.measures': 1
}
const options = {
'stations.$': 1,
'stations.measures': 1
}
What am I doing wrong?
try using these querying params only and after that you will get a user with a requested station..
var user = {
apiKey: req.user.apiKey
}
const query = {
apiKey: user.apiKey,
'stations.stationName': req.params.stationName
}
then do this
User.findOne(query, options)
.exec()
.then(stations => {
for(let station of stations){
if(station.measures[1]){ // here it is the index
res.status(200).send(stations);
}
}
})
.catch(err => {
console.log(err);
res.status(400).send(err);
})
actually in mongoose you cannot query sub-sub documents to you will have to you this approach.. You can only query sub docs only like you have done

populate following users mongoose

Lemme take time to explain what is happening from start to finish.
Preamble:
A user a follows 10 other people. When user A logs in, an X number of posts from each of the 10 people are pulled into view.
I do not know if it is the right thing to do, and will appreciate a better way of doing it. However, I wanna give it a try, and it ain't working.
Follow Model:
let mongoose = require('mongoose');
let Schema = mongoose.Schema;
let FollowSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
followers: [{
type: Schema.Types.ObjectId,
ref: 'Card'
}],
following: [{
type: Schema.Types.ObjectId,
ref: 'Card'
}]
});
module.exports = mongoose.model('Follow', FollowSchema);
Card Model
let mongoose = require('mongoose');
let Schema = mongoose.Schema;
let CardSchema = new Schema({
title: String,
content: String,
createdById: {
type: Schema.Types.ObjectId,
ref: 'User'
},
createdBy: {
type: String
}
});
module.exports = mongoose.model('Card', CardSchema);
Follow logic
When user A follows user B, do two things:
Push the user_id of B to user A document on field 'following' (A is following B)
Push user_id of A to user B document on field 'followers' (B is followed by A)
router.post('/follow', utils.loginRequired, function(req, res) {
const user_id = req.user._id;
const follow = req.body.follow_id;
let bulk = Follow.collection.initializeUnorderedBulkOp();
bulk.find({ 'user': Types.ObjectId(user_id) }).upsert().updateOne({
$addToSet: {
following: Types.ObjectId(follow)
}
});
bulk.find({ 'user': Types.ObjectId(follow) }).upsert().updateOne({
$addToSet: {
followers: Types.ObjectId(user_id)
}
})
bulk.execute(function(err, doc) {
if (err) {
return res.json({
'state': false,
'msg': err
})
}
res.json({
'state': true,
'msg': 'Followed'
})
})
})
Actual DB values
> db.follows.find().pretty()
{
"_id" : ObjectId("59e3e27dace1f14e0a70862d"),
"user" : ObjectId("59e2194177cae833894c9956"),
"following" : [
ObjectId("59e3e618ace1f14e0a708713")
]
}
{
"_id" : ObjectId("59e3e27dace1f14e0a70862e"),
"user" : ObjectId("59e13b2dca5652efc4ca2cf5"),
"followers" : [
ObjectId("59e2194177cae833894c9956"),
ObjectId("59e13b2d27cfed535928c0e7"),
ObjectId("59e3e617149f0a3f1281e849")
]
}
{
"_id" : ObjectId("59e3e71face1f14e0a708770"),
"user" : ObjectId("59e13b2d27cfed535928c0e7"),
"following" : [
ObjectId("59e3e618ace1f14e0a708713"),
ObjectId("59e13b2dca5652efc4ca2cf5"),
ObjectId("59e21942ca5652efc4ca30ab")
]
}
{
"_id" : ObjectId("59e3e71face1f14e0a708771"),
"user" : ObjectId("59e3e618ace1f14e0a708713"),
"followers" : [
ObjectId("59e13b2d27cfed535928c0e7"),
ObjectId("59e2194177cae833894c9956")
]
}
{
"_id" : ObjectId("59e3e72bace1f14e0a708779"),
"user" : ObjectId("59e21942ca5652efc4ca30ab"),
"followers" : [
ObjectId("59e13b2d27cfed535928c0e7"),
ObjectId("59e2194177cae833894c9956"),
ObjectId("59e3e617149f0a3f1281e849")
]
}
{
"_id" : ObjectId("59f0eef155ee5a5897e1a66d"),
"user" : ObjectId("59e3e617149f0a3f1281e849"),
"following" : [
ObjectId("59e21942ca5652efc4ca30ab"),
ObjectId("59e13b2dca5652efc4ca2cf5")
]
}
>
With the above database results, this is my query:
Query
router.get('/follow/list', utils.loginRequired, function(req, res) {
const user_id = req.user._id;
Follow.findOne({ 'user': Types.ObjectId(user_id) })
.populate('following')
.exec(function(err, doc) {
if (err) {
return res.json({
'state': false,
'msg': err
})
};
console.log(doc.username);
res.json({
'state': true,
'msg': 'Follow list',
'doc': doc
})
})
});
With the above query, from my little understanding of Mongoose populate, I expect to get cards from each of the Users in the following array.
My understanding and expectations might be wrong, however with such an endgoal, is this populate approach okay? Or am I trying to solve an aggregation task with population?
UPDATE:
Thanks for the answer. Getting quite close, but still, the followingCards array contains no result. Here's the contents of my current Follow model:
> db.follows.find().pretty()
{
"_id" : ObjectId("59f24c0555ee5a5897e1b23d"),
"user" : ObjectId("59f24bda1d048d1edad4bda8"),
"following" : [
ObjectId("59f24b3a55ee5a5897e1b1ec"),
ObjectId("59f24bda55ee5a5897e1b22c")
]
}
{
"_id" : ObjectId("59f24c0555ee5a5897e1b23e"),
"user" : ObjectId("59f24b3a55ee5a5897e1b1ec"),
"followers" : [
ObjectId("59f24bda1d048d1edad4bda8")
]
}
{
"_id" : ObjectId("59f24c8855ee5a5897e1b292"),
"user" : ObjectId("59f24bda55ee5a5897e1b22c"),
"followers" : [
ObjectId("59f24bda1d048d1edad4bda8")
]
}
>
Here are all the current content I have from Card Model:
> db.cards.find().pretty()
{
"_id" : ObjectId("59f24bc01d048d1edad4bda6"),
"title" : "A day or two with Hubtel's HTTP API",
"content" : "a day or two",
"external" : "",
"slug" : "a-day-or-two-with-hubtels-http-api-df77056d",
"createdBy" : "seanmavley",
"createdById" : ObjectId("59f24b391d048d1edad4bda5"),
"createdAt" : ISODate("2017-10-26T20:55:28.293Z"),
"__v" : 0
}
{
"_id" : ObjectId("59f24c5f1d048d1edad4bda9"),
"title" : "US couple stole goods worth $1.2m from Amazon",
"content" : "for what",
"external" : "https://bbc.com",
"slug" : "us-couple-stole-goods-worth-dollar12m-from-amazon-49b0a524",
"createdBy" : "nkansahrexford",
"createdById" : ObjectId("59f24bda1d048d1edad4bda8"),
"createdAt" : ISODate("2017-10-26T20:58:07.793Z"),
"__v" : 0
}
With the Populate Virtual example from yours (#Veeram), here's the response I get:
{"state":true,"msg":"Follow list","doc":{"_id":"59f24c0555ee5a5897e1b23d","user":"59f24bda1d048d1edad4bda8","following":["59f24b3a55ee5a5897e1b1ec","59f24bda55ee5a5897e1b22c"],"followers":[],"id":"59f24c0555ee5a5897e1b23d","followingCards":[]}}
The followingCards array is empty.
Using the $lookup query on the other hand simply returns []
I'm likely missing something?
You can use either virtual populate or $lookup operator in aggregation pipeline.
Using Virtual Populate
FollowSchema.virtual('followingCards', {
ref: 'Card',
localField: 'following',
foreignField: 'createdById'
});
Follow.findOne({
'user': Types.ObjectId(user_id) })
.populate('followingCards')
.exec(function(err, doc) {
console.log(JSON.stringify(doc));
});
Using $lookup aggregation
Follow.aggregate([
{
"$match": {
"user": Types.ObjectId(user_id)
}
},
{
"$lookup": {
"from": "cards",
"localField": "following",
"foreignField": "createdById",
"as": "followingCards"
}
}
]).exec(function (err, doc) {
console.log(JSON.stringify(doc));
})
var mongoose = require('mongoose'), Schema = mongoose.Schema
var eventSchema = Schema({
title : String,
location : String,
startDate : Date,
endDate : Date
});
var personSchema = Schema({
firstname: String,
lastname: String,
email: String,
dob: Date,
city: String,
eventsAttended: [{ type: Schema.Types.ObjectId, ref: 'Event' }]
});
var Event = mongoose.model('Event', eventSchema);
var Person = mongoose.model('Person', personSchema);
To show how populate is used, first create a person object,
aaron = new Person({firstname: 'Aaron'}) and an event object,
event1 = new Event({title: 'Hackathon', location: 'foo'}):
aaron.eventsAttended.push(event1);
aaron.save(callback);
Then, when you make your query, you can populate references like this:
Person
.findOne({ firstname: 'Aaron' })
.populate('eventsAttended') .exec(function(err, person) {
if (err) return handleError(err);
console.log(person);
});
// only works if we pushed refs to person.eventsAttended
note: change Activity.find to Card.find
const { ObjectID } = require("mongodb");
// import Follow and Activity(Card) schema
const userId = req.tokenData.userId; // edit this too...
Follow.aggregate([
{
$match: {
user: ObjectID(userId)
}
}
])
.then(data => {
// console.log(data)
var dataUsers = data[0].following.map(function(item) {
return item._id;
});
// console.log(dataUsers)
Activity.find(
{ createdById: { $in: dataUsers } },
{
_id: 1,
title: 1,
content: 1,
createdBy: 1,
creatorAvatar: 1,
activityType: 1,
createdAt: 1
}
)
// .sort({createdAt:-1)
.then(posts => res.send({ posts }));
});

Correct way to return from mongo to datatable

I'm using mongoose and returning documents from a collection to be displayed using datatables. I'm having some issues though. The client-side code is
var table = $('#dataTables-example').DataTable( {
"bProcessing" : true,
"bServerSide" : true,
"ajax" : {
"url" : "/mongo/get/datatable",
"dataSrc": ""
},
"columnDefs": [
{
"data": null,
"defaultContent": "<button id='removeProduct'>Remove</button>",
"targets": -1
}
],
"aoColumns" : [
{ "mData" : "name" },
{ "mData" : "price" },
{ "mData" : "category" },
{ "mData" : "description" },
{ "mData" : "image" },
{ "mData" : "promoted" },
{ "mData" : null}
]
});
Then this handled on the server-side using the following
db.once('open', function callback ()
{
debug('Connection has successfully opened');
productSchema = mongoose.Schema({
name: String,
price: String,
category: String,
description: String,
image: String,
promoted: Boolean
});
Product = mongoose.model('Product', productSchema, 'products');
});
exports.getDataForDataTable = function (request, response) {
Product.dataTable(request.query, function (err, data) {
debug(data);
response.send(data);
});
};
If I use the above code the datatable fails to display the documents, claiming no matching records found BUT it does correctly display the number of docs Showing 1 to 2 of 2 entries. If I change the server side code to response with data.data instead of data, the documents are correctly populated in the table BUT the number of records is no longer found, instead saying Showing 0 to 0 of 0 entries (filtered from NaN total entries)
exports.getDataForDataTable = function (request, response) {
Product.dataTable(request.query, function (err, data) {
debug(data);
response.send(data.data);
});
The actual data being returned when querying mongo is
{ draw: '1', recordsTotal: 2, recordsFiltered: 2, data: [ { _id: 5515274643e0bf403be58fd1, name: 'camera', price: '2500', category: 'electronics', description: 'lovely', image: 'some image', promoted: true }, { _id: 551541c2e710d65547c6db15, name: 'computer', price: '10000', category: 'electronics', description: 'nice', image: 'iamge', promoted: true } ] }
The third parameter in mongoose.model sets the collection name which is pluralized and lowercased automatically so it has no effect in this case.
Assuming your Product variable has been declared early on and global, try this:
products = mongoose.model('products', productSchema);
Product = require('mongoose').model('products');
Did you try to remove the dataSrc field in the DataTable configuration:
"ajax" : {
"url" : "/mongo/get/datatable",
},