How to update an array in Mongoose JS - mongodb

Currently, I'm trying to create a Discord bot that will save a to-do list in a database. To do so, I wanted to use an array that contains any items the person adds by typing !listadd (something) that will then be stored in a MongoDB database. However, while this seems to work for Strings, I cannot seem to change an array at all using this method.
await list.findOneAndUpdate({_id: id},{$push:{todoList: msg}, itemToAdd:msg},{upsert : true})
This line above is what I'm using to update the list. While the change to itemToAdd is reflected perfectly in the database, nothing is pushed to todoList. I can't even seem to update todoList at all past setting what the default value is in the schema. (Like, if I put {todoList:['one','two']} as the update object, it still won't do anything)
I've tried using the .updateOne() method and, again, it works fine for itemToAdd, but nothing happens to todoList. I've also tried using the .markModified() method and I've either used it wrong or it isn't working here because, still, nothing changes in the database.
Full Code:
const mongoose = require('mongoose');
const todoSchema = new mongoose.Schema({
todolist : [String],
_id: {
type : String,
required : true
},
itemToAdd:{
type: String,
required: false
},
indexOfItemToRemove:{
type: Number,
required: false
}
});
const list = mongoose.model('list', todoSchema);
module.exports = async (client) => {
client.on('message', async (message) => {
const { author } = message;
const { id } = author;
const { bot } = author;
if (!bot){
console.log('AUTHOR:', author);
if (message.content.includes('!listadd ')){
const msg = message.content.substring(9)
await list.findOneAndUpdate({_id: id},{$push:{todoList: msg}, itemToAdd:msg},{upsert : true})
message.reply('Entry Added!')
}
if (message.content.includes('!listget')){
let doc = await list.findOne({_id:id})
message.reply(doc.itemToAdd)
}
}
})
}

Related

Why does MongoDB give a response stating the document has been updated, but no update it made?

I can't figure this out.
I have an object in an array within another object which I need to update with mongoDB updateOne.
I make the call, it says it found it OK and has updated it ({ n: 1, nModified: 1, ok: 1 }). But then on checking no update is made in the database...
What am I doing wrong here?
Model
const pathwayDetailsSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
associatedPathwayID: {
type: mongoose.Schema.Types.ObjectId,
required: true
},
pages: [
{
_id: { type: String },
x: {
type: Number,
required: true
},
y: {
type: Number,
required: true
},
widgets: [
{
more nested objects..
}
]
}
]
}
Router call
router.post('/pageupdate/',auth, async(req,res)=>{
const pageID = req.body.pageID; //Page ID string
const pathwayID = req.body.pathwayID; // pathwayID string
const update = req.body.update; //{x: new X value, y: new Y value}
try{
console.log("receieved: ",pageID, pathwayID, update);
let updatedDoc = await PathwayDetails.updateOne(
{ associatedPathwayID: pathwayID, "pages._id": pageID },
{ $set: update}
);
console.log("successful? ",updatedDoc)
res.status(201).send(updatedDoc)
}
catch(e){
etc...
}
});
Changing x and y passes through fine and it says it updates. But on checking the database no change is made...
I think you have missed async keyword before await.
A function should be an async inorder to use the await keyword.
So, you wrap the code inside a async function.
Since you are not using the async function, await has lost it's functionality, so it's basically updating the old value again and again. It's not awaiting fo the new value. So you are not seeing any change in the value in the database even though the code is executed successfully.
Try the below code:
const update_document = async (req, res) => {
let updatedDoc = await PathwayDetails.updateOne(
{ associatedPathwayID: pathwayID, "pages._id": pageID },
{ $set: update}
);
res.status(201).send(updatedDoc)
};
After this call the update_document function with the router.
I think this will work.
Figured it out.
The update I was passing wasn't pointing to a nested object correctly.
Rather than update being {x: new X value, y: new Y value}
needed to pass a nested update it must be {"pages.$.x": new X value, "pages.$.y": new Y value}
It is annoying that mongo returns a response saying it has updated the database when it can't find the field to update!

Why does express/mongodb updateOne post allow $set of some values, but will not update others?

I am baffled by this post method, it will update fields 'x and y', but any attempt to set an array of widgets fails.
It is finding the correct item to update, passing all the required information through, but it will not allow insertion of, or update to 'widgets' fields.
Even if I remove the data intended for widgets and arbitrarily send through 'foo' it will not update with a field 'widgets'.
What am I doing wrong here???
API Call to Update Widgets. The Arbitrary X and Y values will update on the database, but any attempt to update widget makes no change
const saveUpdatedWidgets = async (update, _id) => {
console.log("called to update widgets ",update.widgets," in pagecard saveUpdatedWidgets")
let widgetObject = []
for(let u=0;u<update.widgets.length;u++){
widgetObject.push({
id: update.widgets[u].id,
text: update.widgets[u].text
})
}
Api.withToken().post('/pagewidget/'+_id,
{widgets: widgetObject, x:250, y:250}
).then(function (response) {
console.log("?worked ",response.data)
}).catch(function (error) {
console.log("page save failed for some reason on pagecard: ",error.response);
});
};
This will return the following in the console:
Code for post method is:
//THIS ROUTER WILL NOT UPDATE ANY WIDGETS FOR SOME REASON
router.post('/pagewidget/:_id',auth, async(req,res)=>{
console.log("request to update ",req.body," for id ",req.params," in pagewidgetsave post")
const query = { "_id": req.params };
const addedWidgets = req.body;
const newValues = { $set: addedWidgets }
try {
const thePage = await Pages.updateOne( query, newValues);
res.status(201).send(thePage)
console.log("updated Page: ",thePage);
}
catch(e){
console.log(e);
res.status(400).send(e)
}
})
Results from the console running node shows that values are going through, but only x and y actually update in database..
Here is the axios api.js file if there are any issues here:
import axios from 'axios';
const baseURL = process.env.REACT_APP_BASE_URL || "http://localhost:3001"
export default {
noToken() {
return axios.create({
baseURL: baseURL
});
},
withToken() {
const tokenStr = window.sessionStorage.getItem("token")
return axios.create({
baseURL: baseURL,
headers: {"Authorization" : `Bearer ${tokenStr}`}
});
}
}
What is going on!!?? It finds the page OK, and updates x and y values, but can't update widgets, even if the values for widget are just a string or number...
I found the issue. the MongoDB documentation doesn't mention this too well, and in its examples for updateOne() it passes an object for the update argument.
BUT, if you are setting a new field, this argument must be wrapped inside an array to use $set, this is because it can accept both methods to $set and to $unset. (see mongoDB docs)
(i.e. updateOne({query} , [{$set: {field:"value"}, {$unset: {otherfield:"othervalue"}])
In the end the post method just had to change to const thePage = await Pages.updateOne( query, [newValues]); (with newValues stored as an object inside an array, to allow addition of $unset if it was needed.
This is why it would update existing values OK, but it would not set new values into the database.
What a journey....
Full code for post method here
router.post('/pagewidget/:_id',auth, async(req,res)=>{
const query = {"_id": req.params._id};
const addedWidgets = req.body;
const newValues = { $set: addedWidgets }
try {
const thePage = await Pages.updateOne( query, [newValues]);
res.status(201).send(thePage)
console.log("updated Page: ",thePage);
}
catch(e){
console.log(e);
res.status(400).send(e)
}
})

how to get mongoose session in mongoose schema validator

I want to validate a reference in a schema, and I need the validator to be able to access the session.
Scenario
start mongoose session
start mongoose transaction
insert entry to a table
insert entry to another table, with a reference to the first entry
Desired
I want to validate the referenced object exists, but to do that, I need access to the session, inside the validator.
this github issue seems similar, but this.$session() isn't working for me
https://github.com/Automattic/mongoose/issues/7652
I simply don't understand what "this" is supposed to be referring to.
EDIT: adding example
import mongoose from "mongoose";
async function run() {
// User data root schema
const userSchema = new mongoose.Schema(
// Define the data schema
{
accountId: {
type: mongoose.Schema.Types.ObjectId,
required: true,
validate: async (val) => {
console.log("this", this);
}
}
}
);
const User = mongoose.model("User", userSchema);
const url = null; // secret
const options = {};
await mongoose.connect(url, options);
const user = new User({ accountId: "605c662ba2cde486ecd36a4a" });
await user.save();
}
run();
And the output:
this undefined
You shouldn't use a javascript arrow function
An arrow function expression is a compact alternative to a traditional function expression, but is limited and can't be used in all situations.
Differences & Limitations:
Does not have its own bindings to this or super, and should not be used as methods.
Changing it to regular function declaration should fix it:
validate: async function (val){
console.log("this", this);
}

Circular Reference Issue in Mongoose pre-hook

In my MongoDB/Node backend environment I am using Mongoose pre and post hook middleware to check what's changed on the document, in order to create some system notes as a result.
One problem I'm running into is that when I try and lookup the record for the document in question I get a "Customer.findOne()" is not a function error. This is ONLY a problem when I'm looking up a record from the same collection from which the model just launched this pre and post hook triggers file. In other words, if my "Customer" model kicks off functions in a pre hook function in an external file, then I get an error if I then try and lookup a Customer with a standard findOne():
My customer model looks something like this:
module.exports = mongoose.model(
"Customer",
mongoose
.Schema(
{
__v: {
type: Number,
select: false
},
deleted: {
type: Boolean,
default: false
},
// Other props
searchResults: [
{
matchKey: String,
matchValue: String
}
]
},
{
timestamps: true
}
)
.pre("save", function(next) {
const doc = this;
trigger.preSave(doc);
next();
})
.post("save", function(doc) {
trigger.postSave(doc);
})
.post("update", function(doc) {
trigger.postSave(doc);
})
.post("findOneAndUpdate", function(doc) {
trigger.postSave(doc);
})
);
... the problematic findOne() function in the triggers file being called from the model looks like this:
const Customer = require("../../models/customer");
exports.preSave = async function(doc) {
this.preSaveDoc = await Customer.findOne({
_id: doc._id
}).exec();
};
To clarify, this is NOT a problem if I'm using a findOne() to lookup a record from a different collection in this same triggers file. Then it works fine. See below when finding a Contact -- no problem here:
const Contact = require("../../models/contact");
exports.preSave = async function(doc) {
this.preSaveDoc = await Contact.findOne({
_id: doc._id
}).exec();
};
The workaround I've found is to use Mongo instead of Mongoose, like so:
exports.preSave = async function(doc) {
let MongoClient = await require("../../config/database")();
let db = MongoClient.connection.db;
db.collection("customers")
.findOne({ _id: doc._id })
.then(doc => {
this.preSaveDoc = doc;
});
}
... but I'd prefer to use Mongoose syntax here. How can I use a findOne() in a pre-hook function being called from the same model/collection as the lookup type?
I have ran similar issue few days ago.
Effectively it is a circular dependency problem. When you call .findOne() on your customer model it doesn't exist as it is not exported yet.
You should probably try something like that :
const customerSchema = mongoose.Schema(...);
customerSchema.pre("save", async function(next) {
const customer = await Customer.findOne({
_id: this._id
}).exec();
trigger.setPreSaveDoc(customer);
next();
})
const Customer = mongoose.model("Customer", customerSchema)
module.export Customer;
Here customer will be defined because it is not called (the pre hook) before its creation.
As an easier way (I am not sure about it) but you could try to move the Contact import in your Trigger file under the save function export. That way I think the decencies may works.
Did it helps ?

MongoDB: output 'id' instead of '_id'

I am using mongoose (node), what is the best way to output id instead of _id?
Given you're using Mongoose, you can use 'virtuals', which are essentially fake fields that Mongoose creates. They're not stored in the DB, they just get populated at run time:
// Duplicate the ID field.
Schema.virtual('id').get(function(){
return this._id.toHexString();
});
// Ensure virtual fields are serialised.
Schema.set('toJSON', {
virtuals: true
});
Any time toJSON is called on the Model you create from this Schema, it will include an 'id' field that matches the _id field Mongo generates. Likewise you can set the behaviour for toObject in the same way.
See:
http://mongoosejs.com/docs/api.html
http://mongoosejs.com/docs/guide.html#toJSON
http://mongoosejs.com/docs/guide.html#toObject
You can abstract this into a BaseSchema all your models then extend/invoke to keep the logic in one place. I wrote the above while creating an Ember/Node/Mongoose app, since Ember really prefers to have an 'id' field to work with.
As of Mongoose v4.0 part of this functionality is supported out of the box. It's no longer required to manually add a virtual id field as explained by #Pascal Zajac.
Mongoose assigns each of your schemas an id virtual getter by default
which returns the documents _id field cast to a string, or in the case
of ObjectIds, its hexString. If you don't want an id getter added to
your schema, you may disable it passing this option at schema
construction time. Source
However, to export this field to JSON, it's still required to enable serialization of virtual fields:
Schema.set('toJSON', {
virtuals: true
});
I used this :
schema.set('toJSON', {
virtuals: true,
versionKey:false,
transform: function (doc, ret) { delete ret._id }
});
I think it would be great if they automatically suppress _id when virtuals is true.
I create a toClient() method on my models where I do this. It's also a good place to rename/remove other attributes you don't want to send to the client:
Schema.method('toClient', function() {
var obj = this.toObject();
//Rename fields
obj.id = obj._id;
delete obj._id;
return obj;
});
Here is an alternative version of the answer provided by #user3087827. If you find that schema.options.toJSON is undefined then you can use:
schema.set('toJSON', {
transform: function (doc, ret, options) {
ret.id = ret._id;
delete ret._id;
delete ret.__v;
}
});
//Transform
Schema.options.toJSON.transform = function (doc, ret, options) {
// remove the _id of every document before returning the result
ret.id = ret._id;
delete ret._id;
delete ret.__v;
}
there is a "Schema.options.toObject.transform" property to do the reverse or you could just setup as a virtual id.
If you want to use id instead of _id globally then you can set toJSON config on mongoose object(starting from v5.3):
mongoose.set('toJSON', {
virtuals: true,
transform: (doc, converted) => {
delete converted._id;
}
});
Overwrite default method toJSON by new one:
schema.method('toJSON', function () {
const { __v, _id, ...object } = this.toObject();
object.id = _id;
return object;
});
There is also normalize-mongoose a simple package that removes _id and __v for you.
From something like this:
import mongoose from 'mongoose';
import normalize from 'normalize-mongoose';
const personSchema = mongoose.Schema({ name: String });
personSchema.plugin(normalize);
const Person = mongoose.model('Person', personSchema);
const someone = new Person({ name: 'Abraham' });
const result = someone.toJSON();
console.log(result);
So let's say you have something like this:
{
"_id": "5dff03d3218b91425b9d6fab",
"name": "Abraham",
"__v": 0
}
You will get this output:
{
"id": "5dff03d3218b91425b9d6fab",
"name": "Abraham"
}
I created an easy to use plugin for this purpose that I apply for all my projects and to all schema's globally. It converts _id to id and strips the __v parameter as well.
So it converts:
{
"_id": "400e8324a71d4410b9dc3980b5f8cdea",
"__v": 2,
"name": "Item A"
}
To a simpler and cleaner:
{
"id": "400e8324a71d4410b9dc3980b5f8cdea",
"name": "Item A"
}
Usage as a global plugin:
const mongoose = require('mongoose');
mongoose.plugin(require('meanie-mongoose-to-json'));
Or for a specific schema:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const MySchema = new Schema({});
MySchema.plugin(require('meanie-mongoose-to-json'));
Hope this helps someone.
You can also use the aggregate function when searching for items to return. $project will allow you to create fields, which you can do and assign it to _id.
<model>.aggregate([{$project: {_id: 0, id: '$_id'}], (err, res) => {
//
})
If you are using lodash to pick the elements you want, this will work for you.
UserSchema.virtual('id').get(function(){
return this._id.toHexString();
});
UserSchema.set('toObject', { virtuals: true })
UserSchema.methods.toJSON = function() {
return _.pick(
this.toObject(),
['id','email','firstName','lastName','username']
);
Override toJSONmethod for specific model schema.
https://mongoosejs.com/docs/api.html#schema_Schema-method
YourSchema.methods.toJSON = function () {
return {
id: this._id,
some_field: this.some_field,
created_at: this.createdAt
}
}
Create a base schema
import { Schema } from "mongoose";
export class BaseSchema extends Schema {
constructor(sche: any) {
super(sche);
this.set('toJSON', {
virtuals: true,
transform: (doc, converted) => {
delete converted._id;
}
});
}
}
Now in your mongoose model, use BaseSchema instead of Schema
import mongoose, { Document} from 'mongoose';
import { BaseSchema } from '../../helpers/mongoose';
const UserSchema = new BaseSchema({
name: String,
age: Number,
});
export interface IUser {
name: String,
age: Number,
}
interface IPlanModel extends IUser, Document { }
export const PlanDoc = mongoose.model<IPlanModel>('User', UserSchema);
Typescript implementation of #Pascal Zajac answer
There's another driver that does that http://alexeypetrushin.github.com/mongo-lite set convertId option to true. See "Defaults & Setting" section for more details.
Mongoose assigns each of your schemas an id virtual getter by default which returns the document's _id field cast to a string, or in the case of ObjectIds, its hexString.
https://mongoosejs.com/docs/guide.html
You can also use pre 'save' hook:
TouSchema.pre('save', function () {
if (this.isNew) {
this._doc.id = this._id;
}
}
JSON.parse(JSON.stringify(doc.toJSON()))