Is it possible to get a detailed query when delete operation fails? - node.js

I am doing a delete operation with a filter of 2 fields:
const query = await Flow.deleteOne({
_id: flowId,
permissions: currentUser!.id,
});
Then I check the query object that returns from this operation to determine whether the operation was successful or not:
if (!query.deletedCount) {
throw new BadRequestError("Flow not found");
}
The problem is that I cant know if the operation failed because the flowId is wrong (first filter field) or the user don't have permissions (second filter field).
Is there an option to get a more detailed query result in mongoose?

As you can see from the official docs, deleteOne returns an object with three properties:
ok 1 if no errors
deletedCount number of docs deleted
n number of docs deleted
If you need to investigate the reasons for the failure you could query the database in case of error, something like:
if (!query.deletedCount) {
const flow = await Flow.findById(flowId);
// wrong `flowId`
if (!flow) throw new BadRequestError("Flow not found");
// otherwise, user don't have permission
throw new BadRequestError("Unauthorized");
}

Related

How to avoid getting "Cast to ObjectId failed for value" after passing an invalid id to a query function in mongoose?

I have a route handler that take the id parameter from request object and uses it to find a document.
I thought that the findById function would return null if it doesn't find any document with the given id. So I created an if condition which generate an error with the appropriate message and status code. But it turns out the function automatically returns the following error if the id is invalid
But I do not want the "findById" query to generate error by itself. I want the tour variable to be null if the id is invalid so that my own implementation for handling 'Not found' exception works. How may I do so?
Try wrapping the line const tour = await Tour.findById(req.params.id) with a try catch statement like this:
try {
const tour = await Tour.findById(req.params.id);
} catch (error) {
next(new AppError(error.message, 404));
}
this won't solve the problem completely as you still need to convert the value of the id you're trying to query (I assume you're receiving a string) to a mongoose ObjectId. see this answer for this

Maintain consistency in concurrent read operations in MongoDB

I have an API in Node.js say /create/appointment which does 2 operations
STEP 1: Check for active appointment in DB.
STEP 2: Create an appointment if not exist.
I have 3 collections
doctors
patients
appointments
appointments collection has 3 main fields
doctor_id: MongoDb ObjectID
patient_id: MongoDB ObjectID
is_active: Boolean
STEP 1 contains one DB read operation.
STEP 2 contains one DB write operation.
When the API is fired multiple times simultaneously,
STEP 1 of second cycle is executed before STEP 2 of first cycle completes. Since STEP 2 of first cycle is not completed, STEP 1 of second cycle does not return the active appointment entry created in first cycle.
I cannot use compound unique index on:
{doctor_id, patient_id}, since appointment collection can contain historical data.
{doctor_id, patient_id, is_active}, then appointment collection can contain only one inactive entry.
Implementation:
// All functions below return promise object.
doctorManager
.getDoctor(doctor_id)
.then(doctor => {
// throw error if doctor does not exist
return patientManager.getPatient(patient_id);
})
.then(patient => {
// throw error if patient does not exist
return getActiveAppointment(doctor_id, patient_id)
})
.then(activeAppointment => {
// throw error if active appointment exist
return appointmentManager.createAppointment(doctor_id, patient_id)
})
.then(() => {
// return API response
})
.catch(error => {
// handel error
});
Is there a way to lock appointment collection when any sort of operation is going on, or any other better solution.
I cannot shard my DB nor setup replication.
Here's something it will look when you use async await functionality. Its not problem solution but just an example of handling your problem properly.
doctor = await doctorManager.getDoctor(doctor_id);
patient = await patientManager.getPatient(patient_id);
appointment = await doAppointmentExists(doctor_id, patient_id);
if(isEmpty(appointment)) {
create appointment = await appointmentManager.createAppointment(doctor_id, patient_id)
} else {
throw new Error("Duplicate Appointment");
}

Mongoose Error while performing delete

I am running into following error but I unable to completely grasp the understanding behind the error.
CastError: Cast to ObjectId failed for value "XYZ" at path "_id" for model "Partner"
I have my schema defined as following
var partnerList = new Schema (
{
partnerName: String,
supportedProducts: [String]
},
{
collection: 'partnerList'
}
);
module.exports = mongoose.model('Partner', partnerList);
The functionality of my delete function
delete: function (req, res) {
var removePartner = req.params.partnerName;
var promise = Partner.findByIdAndRemove(removePartner).exec();
promise.then(function removePartner(val) {
console.log('partner value removed');
res.send(val);
}).catch(function catchError(err){
console.error(err);
throw err;
});
}
I am trying to making a request to my node app service using
localhost:8443/data/XYZ, where i am passing the value 'XYZ' as the parameter. This value is used to delete the appropriate object from the database.
Basically the error means that whatever you pass as your "XYZ" url param is not a valid ObjectId.
Guessing from your code you use the "partner name" (probably some arbitrary string) instead of the database id of the partner. However findByIdAndRemove() requires you to specify an ObjectId as it uses this to identify which document to delete:
Model.findByIdAndRemove(id, [options], [callback])
Your delete API call could then look something like this: http://localhost:8443/data/59558ccd7acc4dd63ea88988. However for this the client needs to know the ObjectId of the partner.
So you have to either use the ObjectId of a partner in the URL, or use remove() to implement your custom delete query instead, for example like this (if name is the property you use to store your partner names):
Partner.remove({ name: partnerName }).exec();
Be careful however that this might remove multiple documents if your partner name is not unique, as remove will delete all documents matching the query.
In order to prevent this you can also use findOneAndRemove() using the same query. This would only remove one document at a time. If there are multiple partners with the same name it would remove the first one (depending on your sort order).

MongoDB/Mongoose returning empty array

I'm currently working on a project with mongodb/mongoose, and every time I purposely query for something that does not exist in the DB, I am getting a response with an empty array. This is my code for using Express to set up an API and return the data found in the DB:
app.get('/api/:id', function(req, res) {
var id = req.params.id;
Job.find({jobID: id}, function (err, foundJob) {
if (err) {
console.log(err);
}
else {
res.json(foundJob);
}
});
});
However, every time I go to localhost:3000/api/67 (I have no object with jobID: 67 in the database yet), the console does not print the error. It gives me a JSON response with an empty array. Any ideas to why this is happening? The weird part is that when I change jobID: id to _id: id, it does give me an error. Why doesn't it do that for the jobID field?
EDIT: Just to clarify, the reason why I do not want this behavior is because my program will never print the error, even if a job in my DB doesn't exist with that specified jobID.
It does something different than you think it does.
Job.find({jobID: id}, ... )
What this really says is give me array with all the documents in collection "Job" that have field "jobID" equal to some value.
What if there is no such document? Well, then the array will be empty. Without any error, of course, what should be an error here? You asked for all documents (given some filter) and an array with all such documents was returned. It is just a coincidence that the size of the array is zero, because there are no such documents.
If you want to check whether there is no such document then check whether the array is empty.
I don't know why it is giving you error when you change JobID to _id; what error exactly is it?
If you are interested only in one document, then there is method findOne that returns only the first document (or null if no such documents exist) instead of an array.
About error when you try to find something by it's __id: It gives you a error because __id is not String, it's ObjectId. So you should search for document with that __id like this: _id: ObjectId(id)
About empty string: If you want to display some kind of different message you should check if db returned something or is it rather empty string that got returned. Something like this:
app.get('/api/:id', function(req, res) {
var id = req.params.id;
Job.find({jobID: id}, function (err, foundJob) {
if(foundJob){
res.json(foundJob);
}else{
res.json("nothing found");
}
if (err) {
console.log(err);
}
});
});
Edit: I didnt realize that you had check for error, I changed code.
Returning an empty string is normal behavior for mongoose. You should handle your response like this:
if (err) {
//handle error
} else if (foundJob) {
//work with your data
} else {
//nothing was found
}
The error you get with _id must be irrelevant and is probably due to an invalid query.

nodejs: save function in for loop, async troubles

NodeJS + Express, MongoDB + Mongoose
I have a JSON feed where each record has a set of "venue" attributes (things like "venue name" "venue location" "venue phone" etc). I want to create a collection of all venues in the feed -- one instance of each venue, no dupes.
I loop through the JSON and test whether the venue exists in my venue collection. If it doesn't, save it.
jsonObj.events.forEach(function(element, index, array){
Venue.findOne({'name': element.vname}, function(err,doc){
if(doc == null){
var instance = new Venue();
instance.name = element.vname;
instance.location = element.location;
instance.phone = element.vphone;
instance.save();
}
}
}
Desired: A list of all venues (no dupes).
Result: Plenty of dupes in the venue collection.
Basically, the loop created a new Venue record for every record in the JSON feed.
I'm learning Node and its async qualities, so I believe the for loop finishes before even the first save() function finishes -- so the if statement is always checking against an empty collection. Console.logging backs this claim up.
I'm not sure how to rework this so that it performs the desired task. I've tried caolan's async module but I can't get it to help. There's a good chance I'm using incorrectly.
Thanks so much for pointing me in the right direction -- I've searched to no avail. If the async module is the right answer, I'd love your help with how to implement it in this specific case.
Thanks again!
Why not go the other way with it? You didn't say what your persistence layer is, but it looks like mongoose or possibly FastLegS. In either case, you can create a Unique Index on your Name field. Then, you can just try to save anything, and handle the error if it's a unique index violation.
Whatever you do, you must do as #Paul suggests and make a unique index in the database. That's the only way to ensure uniqueness.
But the main problem with your code is that in the instance.save() call, you need a callback that triggers the next iteration, otherwise the database will not have had time to save the new record. It's a race condition. You can solve that problem with caolan's forEachSeries function.
Alternatively, you could get an array of records already in the Venue collection that match an item in your JSON object, then filter the matches out of the object, then iteratively add each item left in the filtered JSON object. This will minimize the number of database operations by not trying to create duplicates in the first place.
Venue.find({'name': { $in: jsonObj.events.map(function(event){ return event.vname; }) }}, function (err, docs){
var existingVnames = docs.map(function(doc){ return doc.name; });
var filteredEvents = jsonObj.events.filter(function(event){
return existingVnames.indexOf(event.vname) === -1;
});
filteredEvents.forEach(function(event){
var venue = new Venue();
venue.name = event.vname;
venue.location = event.location;
venue.phone = event.vphone;
venue.save(function (err){
// Optionally, do some logging here, perhaps.
if (err) return console.error('Something went wrong!');
else return console.log('Successfully created new venue %s', venue.name);
});
});
});

Resources