Unable to update data values on mongoDb native findOneAndUpdate query - javascript

exports.update_activity_status = (req, res) => {
const {
campaign_id,
leadId,
leadActivity,
status,
} = req.body;
const client = mongoClient.connect(`${process.env.MONGO_URL}`, {
useUnifiedTopology: true,
});
client.then((cli) => {
cli
.db(`${process.env.DATABASE_NAME}`)
.collection(`${process.env.ACTIVITY_COLLECTION}`)
.findOneAndUpdate(
{
"data.campaign_id": mongoose.Types.ObjectId(campaign_id),
"data.leads.$._id": mongoose.Types.ObjectId(leadId),
// "data.leads._id" : leadId
},
{
$set: {
"data.leads.$.leadActivity": leadActivity,
"data.leads.$.status": status,
},
},
{
returnNewDocument: true,
}
)
.then((result) => {
console.log("UPDATED RESULT", result);
res.json(result)
})
.catch((err) => console.log("new err", err));
});
};
In my query, I need to update the status and leadActivity of the users whose leadId matches the following conditions:
campaign_id that matches the document (this is working fine).
leadsId that matches inside that particular document (not working).
First I tried using only db.find({"data.campaign_id": mongoose.Types.ObjectId(campaign_id)}) and it returns me the data that matches only the first condition as specified above. As soon as I try it along with second condition and findOneAndUpdate({....}), accessing the nested data after including secondary filter condition returns null.
Here's is what my document object looks like:
Any help to resolve this is appreciated. Thanks in advance.

The second condition for matching the _id in the leads-array is incorrect, you need to change it to:
"data.leads._id": mongoose.Types.ObjectId(leadId)

Related

Mongoose: attempting to remove last element from an array results in seemingly bizarre behavior

Update: I switched from updateOne and $pull to simply filtering the array and saving it. I don't know why but that solves the first issue of html elements being removed. The same error occurs when deleting the last Item in a Menu however.
I have the following Express router to remove an Item element from an array in a Menu:
router.put('/menus/:menuid/items/:itemid', async (req, res, next) => {
console.log('attempting to delete menu item...')
const menu = await Menu.findById(req.params.menuid)
const item = menu.items.find(item => item.id === req.params.itemid)
console.log(item)
console.log('updating...')
const response = await Menu.updateOne(
{ _id: menu._id },
{ $pull: { items: { _id: item._id } } }
)
console.log(response)
req.menu = await Menu.findById(req.params.menuid)
})
This successfully removes the desired array element, however the function that called fetch() on this request doesn't proceed with then(); the elements on the page don't change until I refresh the browser:
function redirectThenDeleteElement(path, id, method) {
console.log("redirect to " + path + " using " + method)
fetch(path, { method: method })
.then(response => {
if (response.ok) {
console.log('fetch successful')
const itemToDelete = document.getElementById(id)
itemToDelete.remove()
} else {
console.log('error with request')
}
}).catch(error => {
console.error('error with fetch call: \n' + error)
})
}
From here it gets weirder. If i delete the last item from a Menu, it behaves as expected. Same if I add a new item then delete it again.
But if I delete all the items from one Menu then delete the last item from another, "updating..." is logged and I get the error:
MongoServerError: E11000 duplicate key error collection: pfa-site.menus index: items.title_1 dup key: { items.title: null }
This also happens if I seed one Menu with an empty items array and then delete the last item from another Menu. It refers to items.title_dup/items.title, and I don't have a coherent idea why it would, and I don't know what an index means in this context.
My first thought was that if items.title was meant to be the title property of Item, which is unique, the error makes sense if multiple menus are for some reason trying to update their items.title property to null. But the result is the same if I remove the unique parameter from the schema:
const menuItemSchema = new mongoose.Schema({
title: {
type: String,
required: true,
unique: false
},
content: String,
sanitizedHtml: {
type: String,
required: true
},
slug: {
type: String,
required: true,
unique: true
}
})
const menuSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
order: {
type: Number,
required: true
},
items: {
type: [menuItemSchema]
},
slug: {
type: String,
required: true,
unique: true
}
})
menuSchema.pre('validate', function(next) {
if (this.title) {
this.slug = slugify(this.title, { lower: true, strict: true })
}
this.items.forEach((item) => {
console.log('content being sanitized...')
if (item.content) {
item.sanitizedHtml = dompurify.sanitize(marked.parse(item.content))
}
if (item.title) {
item.slug = slugify(item.title, { lower: true, strict: true })
}
})
next()
})
module.exports = mongoose.model('Menu', menuSchema)
Maybe the weirdest part is that if I add a callback function to updateOne that simply logs any error/result, and then attempt to delete any item under any condition, i get the error:
MongooseError: Query was already executed: Menu.updateOne({ _id: new ObjectId("63b3737f6d748ace63beef8a... at model.Query._wrappedThunk [as _updateOne]
Here is the code with the callback added:
router.patch('/menus/:menuid/items/:itemid', async (req, res, next) => {
console.log('attempting to delete menu item...')
const menu = await Menu.findById(req.params.menuid)
const item = menu.items.find(item => item.id === req.params.itemid)
console.log(item)
console.log('updating...')
const response = await Menu.updateOne(
{ _id: menu._id },
{ $pull: { items: { _id: item._id } } },
function(error, result) {
if (error) {
console.log(error)
} else {
console.log(result)
}
}
)
console.log(response)
req.menu = await Menu.findById(req.params.menuid)
})
Thank you for taking the time to read all of this. You're a real one and I hope you have any idea what's happening!
I've tried using findOneAndUpdate instead of updateOne and using different http request methods, among other things that failed and I forgot about. Results are similar or broken for I think unrelated reasons.
I think this is related to how you handle your requests.
As a better and cleaner code, I would suggest making the "put" method to be "delete" so the api will explicitly be meant for deleting an item from a menu.
Also, I don't know where the result is being sent back to the frontend.
I would suggest something like that:
router.delete('/menus/:menuid/items/:itemid', async (req, res, next) => {
console.log('attempting to delete menu item...')
const {menuid, itemid} = req.params;
const menu = await Menu.findById(menuid)
const item = menu.items.find(item => item.id === itemid)
if(!item){
throw new Error('item not found')
}
console.log(item)
console.log('updating...')
const response = await Menu.updateOne(
{ _id: menu._id },
{ $pull: { items: { _id: itemid } } },
{ new: true}
)
console.log(response)
return res.status(200).json(response);
})
Solution: it turns out that since the Item schema had unique fields, MongoDB automatically added indices to the parent schema where the index name is e.g. title_1 and the key:value, in the case of an empty items array, is 'items.title': null. So when the last item is deleted from a second menu some conflict happens where there are duplicate indices? I'm not sure, so any clarification on that would be helpful.
Anyway, the solution was to add sparse: true in addition to unique: true. This directly prevents an index from having a null value. Again, further clarification on exactly how this works would be great. See BahaEddine Ayadi's comment for further details.

Sequelize update via PATCH, how to process each possible result

I'm creating a rest api for CRUD operations using Sequelize and MySql. I'm using a controller to run an update on a PATCH request to update fields of a product. It technically works, but I feel like there is a more elegant way to handle this.
Sequelize's update method will return an array of objects depending on the results. Array[0] is the number of rows affected by the update (should just be one in my case, as I'm updating by id). Array[1] will return an object with details about the update as well as all the old values and new values. Here's how I'm handling that currently:
//products.controller.js
//Update a single product using id (PUT/PATCH)
const patch = (req, res) => {
const id = req.params.id;
Product.update(req.body, { where: { id }, individualHooks: true })
.then((rowsAffected) => {
//Item not found
if (Object.entries(rowsAffected[1]).length === 0) {
res.status(404).send({
success: false,
status: 404, //Not found
message: `Product with id ${id} not found. Update failed.`,
});
return;
}
//if rowsAffected[0] === 1 then success
if (rowsAffected[0] === 1) { //row changed
res.status(200).send({
success: true,
status: 200,
message: `Product updated.`,
id: id,
payload: req.body,
});
} else {
// if rowsAffected[0] !== 1 then it failed.
res.status(200).send({
success: false,
status: 200, //Not Modified
message: `No fields have changed. Product not updated.`,
});
}
})
.catch((err) => {
res.status(500).send({
success: false,
status: 500,
message:
err.message || "Something went wrong while updating the product.",
});
});
}
As you can see, first I'm checking to see if the the update function returns the product details (meaning it successfully found it in the database). If not then sending 404. Then I check the affected rows. If 1 then success, if 0 then nothing changed. Finally I'm catching any server errors.
I feel like there is a better way rather than having to break down the update function's return (like Object.entries(rowsAffected[1]).length === 0)
This is ok if this is the only way you can check the effects of the update. What I can suggest is putting an abstraction above it.
First thing that checking (rowsAffected[0] === 1) does not make much sense, since the update is idempotent and you end up with the same resource state no matter what the actual values are. If you insist, then I would not pair success: false with a 200 ok status, because failure is failure and it requires an error message and 4xx or 5xx status. So either delete it or convert it into a proper error. Hard to find such a status code, but maybe using 409 conflict is ok in these cases https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409 though I would just remove this part of the code. I keep it for the sake of the example.
As of the success and status properties in the body, they don't make much sense either, because they travel in the header, and it is evident from the HTTP standard that 2xx means success, 4xx and 5xx means error. So I would remove those too.
If you don't want to support detailed error codes and exception types and parameters, then just send the error messages and the body can be even a string instead of an object.
Sending the err.message to the consumers is a bad idea by unexpected errors. You don't know what you send out. You need to log them and send something general instead. Communicating errors is always a higher abstraction level stuff, many times. As of the Product with id ${id} not found. Update failed. here adding the id is not necessary, because the request contains it.
So atm. the code looks like this:
const patch = (req, res) => {
const id = req.params.id;
Product.update(req.body, { where: { id }, individualHooks: true })
.then((rowsAffected) => {
if (Object.entries(rowsAffected[1]).length === 0) {
res.status(404).send({message: `Product not found. Update failed.`});
return;
}
//if rowsAffected[0] === 1 then success
if (rowsAffected[0] === 1) { //row changed
res.status(200).send({
message: `Product updated.`,
id: id,
payload: req.body,
});
} else {
res.status(409).send({message: "No fields have changed. Product not updated."});
}
})
.catch((err) => {
res.status(500).send({message: "Something went wrong while updating the product."});
});
}
We can go further by mapping status codes to status messages and extracting the possibly repeating parts of the story into separate functions.
const patch = (req, res) => {
const id = req.params.id;
const statusMessages = {
200: "Product updated."
404: "Product not found. Update failed."
409: "No fields have changed. Product not updated.",
500: "Something went wrong while updating the product."
};
Product.update(req.body, { where: { id }, individualHooks: true })
.then(updateStatusVerification)
.then(successHandler(res, statusMessages, () => {
return {
id: id,
payload: req.body,
};
}))
.catch(apiErrorHandler(res, statusMessages));
}
function successHandler(res, statusMessages, callback){
return function (){
let body = callback();
body.message = statusMessages[200];
res.status(200).send(body);
};
}
function apiErrorHandler(res, statusMessages){
return function (err){
let statusCode = 500;
if (err instanceof NotFoundError)
statusCode = 404;
else if (err instanceof NotUpdatedError)
statusCode = 409;
res.status(statusCode).send({
message: statusMessages[statusCode]
});
};
}
function updateStatusVerification(rowsAffected){
return new Promise((resolve, reject) => {
if (Object.entries(rowsAffected[1]).length === 0)
reject(new NotFoundError);
else if (rowsAffected[0] !== 1)
reject(new NotUpdatedError);
else
resolve();
});
}
class ApiError extends Error {}
class NotFoundError extends ApiError {}
class NotUpdatedError extends ApiError {}
We can move the status messages to the documentation. So you will end up with something like this and some utility functions:
const patch = (req, res) => {
const id = req.params.id;
statusMessages = docs.product.update.statusMessages;
Product.update(req.body, { where: { id }, individualHooks: true })
.then(updateStatusVerification)
.then(successHandler(res, statusMessages, () => {
return {
id: id,
payload: req.body,
};
}))
.catch(apiErrorHandler(res, statusMessages));
}
We can go even further if this is a frequent pattern:
const patch = (req, res) => {
const id = req.params.id;
handleUpdate(
Product.update(req.body, { where: { id }, individualHooks: true }),
() => {id: id, payload: req.body},
docs.product.update.statusMessages
);
}
function handleUpdate(dbUpdatePromise, successCallback, statusMessages){
dbUpdatePromise.then(updateStatusVerification)
.then(successHandler(res, statusMessages, successCallback))
.catch(apiErrorHandler(res, statusMessages));
}
So it can be as abstract as you like, it really depends on your needs and what the current usage allows. You can decide how many and what kind of layers you need based on actual use cases and repetitions.

Res value is null in an app.get call done from vue.js front-end to express back-end

I am calling this code from the front-end and confirmed that there is a proper db connection and that the Id value is properly passed, and that there is a corresponding value in the database, but for some reason, res is null. What am I missing?
app.get("/api/walletlogin/user/:userId", (req, res) => {
id = req.params.userId
var query = {_id: id}
db.collection("Users").findOne(query, (err, result) => {
if (result) {
console.log(result.userName)
} else {
console.log('No User')
}
})
Here is the front-end call:
axios.get('/api/walletlogin/user/' + accounts)
.then((response) => {
console.log('Logged in With ' + accounts)
router.push('/account')
})
.catch((errors) => {
console.log('Cannot log in')
})
}).catch((err) => {
console.log(err, 'err!!')
})
You could try to convert your id to an objectID.
var ObjectId = require('mongodb').ObjectId;
var id = ObjectId(req.params.userId);
to search by id, you must use the ObjectID class from the mongodb package. Here is an example invented by me, it does not reflect the real work, but I hope it will become clear on it:
const { ObjectID } = require('mongodb');
const id = '5ee4f69bfa0b960de8aec158'; // in your example is req.params.userId
db.collection('users').findOne({ _id: new ObjectID(id)}, (error, result) => {
if (error) {
throw error;
}
console.log(result);
})
I am adding the details of the issue initially encountered in case someone else would experience it in the future. The value that is passed from the front-end is a cryptocurrency address. For some reason, some of the characters passed were upper-case, while the same address had been stored in the database with these same characters as lower case. Thus, one needs to add code to make sure that the case of the letters found in the respective addresses is ignored.
J

How to use forEach when iterating over collection objects MongoDB?

please suggest me how to make a selection from database comparing the ID in the collection with each element of the array?
Here is the code that unfortunately returns an empty array:
index(req, res) {
Room.find({_id: req.user.rooms.forEach((item)=>{
return item;
})
})
.then((rooms) => {
console.log(rooms)
res.send(rooms)
}
)
.catch(err => res.status(400).json(err));
}
req.user.rooms - each item of this array is ID, that I want to compare with what is in the collection Room.
It's pretty straight-forward in their docs for how to query items in a list.
Your code should look something like this:
index(req, res) {
// Additional validation should be done to make sure that req.user.rooms
// is an array with length > 0. I'll leave that for you to do.
const rooms = req.user.rooms;
Room.find({ _id: rooms})
.then((rooms) => {
console.log(rooms)
res.send(rooms)
})
.catch(err => res.status(400).json(err));
}
Going beyond that, you should really not be doing DB queries from your controller; it's not a good architectural practice This is how it could look in your node service
roomController.js
const RoomService = require("/path/to/services/directory"); // Let services contain all business logic. Don't give them anything related to your web server framework
async index(req, res) {
// Additional validation should be done to make sure that req.user.rooms
// is an array with length > 0. I'll leave that for you to do.
try {
const rooms = await RoomService.retrieveById(req.user.rooms);
res.send( { success: true, data: rooms } ); // We send back the result when we get one
} catch ( err ) {
// We respond to the client with our error, ideally you'll log it too
res.status( err.statusCode ).send( { success: false, error: err } );
}
}
RoomService.js
const Room = require("/path/to/your/model");
module.exports = {
retrieveById: async function(ids) {
try {
return await Room.find({ _id: ids}); // Typically this would be abstracted to a DB layer. but putting it here for brevity
} catch( err ) {
throw new Error( err ); // This is caught in our controller, which we send to client
}
}
};

Sequelize - update query with returning: true succeeds but returns undefined

I have the following function which I use to update the URL to the user's profile pic -
const updateProfilePic = async (req, res) => {
const userId = req.param("id");
if (userId) {
const targetPath = `...`;
User.update(
{
profilePic: targetPath
},
{ where: { id: userId }, returning: true }
)
.then(updatedUser => {
console.log(updatedUser);
res.json(updatedUser);
})
.catch(error => {
console.log(error);
res.status(400).send({ error: error });
});
}
};
This updates the DB successfully - however then obtains [ undefined, 1 ] as the updatedUser instead of the user data - if I remove the returning : true flag, it just returns [ 1 ]. I'm not sure what I'm doing wrong - I'm trying to obtain the user object so that I can pass it on to the client.
I presume you are not using Postgres.
The returning property in options is only supported in Postgres.
Update() returns an array with one or two elements. The first element is the number of affected rows. The second array element is only supported in postgres and will return the updated row.
So you are getting the 1 returned which is the number of rows updated.
Docs

Categories