Maintain consistency in concurrent read operations in MongoDB - node.js

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");
}

Related

Is it possible to get a detailed query when delete operation fails?

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");
}

Firebase: How to get document in sub collection without the parent key

I need to perform a query to get the oldest document in a sub collection.
I want to perform this query with few reads as possible.
DB description:
Based on Firebase.
Collection of devices. Each device holds a collection of call-backs. For a specific device I need to fetch the oldest call-back (call-backs has timestamp).
I think I know how to perform this query using the device Unique ID, But I want to do it by filtering by some field of the device, this field is also unique.
I was able to do it by querying the device with all of his call-backs but this will charge me for more reads then actually needed.
Query that works using ID:
admin
.firestore()
.collection("devices/{device_id}/callbacks")
.{order_by_timestamp}
.limit(1)
.get()
.then((data) => {
let callbacks = [];
data.forEach((doc) => {
callbacks.push(doc.data());
});
return res.json(callbacks);
})
.catch((err) => console.error(err));
If that field in devices collection is unique then you can fetch ID of that device first and then proceed with your existing logic as shown below:
async function getOldestCallback(thatFieldValue) {
const device = await admin.firestore().collection("devices").where("thatField", "==", thatFieldValue).get()
if (device.empty) return false;
const deviceId = device[0]["id"];
// existing function
}
This should incur 2 reads (1 for device document and 1 for oldest callback if it exist).
Additionally, since you are limiting number of docs to be returned to 1 then you can use [0] instead of using a forEach loop.
const callbacks = [ data[0].data() ]

How to get deleted documents from DeleteMany middleware in Mongoose?

I have an application in which I manage categories and subcategories.
So each time I delete a category, I have a mongoose middleware that deletes all the subcategories who belong to it.
schema.post('findOneAndDelete',
(category: CategoryDocument) => subcategoryModel.deleteMany({ category: category.id }))
That works fine when I delete just one category, what happens when I delete multiple?
I tried registering a deleteMany middleware like so:
schema.post('deleteMany',
(deletedQueryResult: any) => {})
But the deleteMany middleware just sends to my callback the result of the query (the time it took, number of deleted documents and some internal mongoose/mongodb properties).
Is there anyway I can get the deleted documents or their ids inside a mongoose middleware?
If not what are my other options here?
The deleteMany method returns an object with three fields:
n – number of matched documents
ok – 1 if the operation was successful, else 0
deletedCount – number of deleted documents
This is the same behaviour of the mongodb db.collection.deleteMany method.
I suggest you to add a static method to your model:
// Assign a function to the "statics" object of our schema
categorySchema.statics.deleteManyWithSubs = async function(catIds) {
const deleteQuery = /*your delete query using catIDs*/
const delRes = await this.deleteMany(deleteQuery);
catIds.forEach(id => {
await subcategoryModel.deleteMany({ category: id })
});
return true;
};
// Usage
categoryModel.deleteManyWithSubs(ids)...

How can I sort my mongoose response by the amount of documents (on another collection) that has a reference to the object of the response?

I am programming a back-end in node.js with mongoose to communicate with my mongo database.
At the moment I have two collections: threads and comments.
Comments contain a field named thread which contains the objectID of a document of the threads collection.
Now I would like to sort all threads in descending order on the amount of comments.
So now I have:
Thread.find({})
.then((threads) => {
function mycomparator(a, b) {
Comment.find({ thread: a._id }).countDocuments()
.then((amountOfCommentsOnA) => {
Comment.find({ thread: b._id }).countDocuments()
.then((amountOfCommentsOnB) => {
return amountOfCommentsOnB - amountOfCommentsOnA;
})
})
};
threads.sort(mycomparator);
res.send(threads);
})
But the sorting is not working. When I console.log the amountOfCommentsOnA and amountOfCommentsOnB the result is right. But as I said, the sorting is not.
What do I need to make this working?

How to use mongoose "populate" to specify the path for already existing document of different collection?

I am using Apollo Graphql, Express-Nodejs,MongoDB and Mongoose. I have 2 collection namely: Business and Order.
Here are the models
Here are the graphql types:
Here are the mutation:
createBusiness(
name: String,
address: String,
): Business
createOrder(
orderNumber: String,
businessName: String,
additionalDetails: String
): Order
A particular Business can have multiple orders, A particular order must have one particular Business.
What I want to do is to create an order for Business document.
Case 1.) If the Business document doesn't exists: then the createOrder mutation should create new Business document (by using populate)
Case 2.) But If the Business document exists, then the createOrder mutation should not create new Business document and only add new order and the reference to the existing Business document.
Could someone please let me know how can I fulfill the above in the graphql and mongoose ? Any suggestion would be helpful !
Here is my Order mutation resolver ( Its not working, not sure why !! )
import Order from '../models/Order';
import Business from '../models/Business';
export default {
Mutation:{
createOrder(_, {
orderNumber,
additionalDetails,
businessName
}){
return Business.findOne({
businessName: businessName
})
.then((exist)=>{
if (!exist){
let business_Name = new Business({
name: businessName
})
business_Name.save(function (err){
if (err) return handleError(err);
let order = new Order({
orderNumber: orderNumber,
businessName: business_Name._id,
additionalDetails: additionalDetails
});
order.save(function (err){
if (err) return handleError(err);
});
});
}
if (exist){
// WHAT SHOULD I DO FOR THIS CASE ??
}
});
},
}
}
Thanks in advance !
I slightly modified the logic for the case 1 such that if Business name doesn't exist then one should not be allowed to create Order. As if someone unauthorized is allowed to create business in the order mutation, then we may have to handle additional optional arguments(only for case 1) making the "createOrder" mutation to be more cumbersome and not so logical.
Rather we would inform the client user with some useful msg for the case 1 and for the case 2 when the business exist, we would:
1.) First create new Order then push it to the "orders" list of type Business and then save it (As the Type Business needs this reference to its child array of orders) (Read this: Saving Refs to Children )
2.) Then its time to save the newly created order and populate "businessReference".
Here is the complete code of createOrder mutation...
createOrder: async(_, {
orderNumber,
additionalDetails,
businessName
})=>{
// Check if the business name exists or not
try {
const business_name = await Business.findOne({
name: businessName
})
if (!business_name){
throw new Error ('Business name not found. Please create the Business first !');
}
// if the business name exists, then
// first create order
let order = await new Order({
orderNumber: orderNumber,
businessReference: business_Name._id,
additionalDetails: additionalDetails
})
business_name.orders.push(order); // then push this order to child
business_name.save(); // array of Business for referencing
// it later
return order.save() //then save the order,
.then(res => Order.findById(res._id) // and populate
.populate('businessReference')
.exec())
}
catch (error) {
throw error;
}
}
Since the exec() will return promise only if it doesn't have any arguments, so I returned it this way. For more info regarding this, please look into
this awesome explained stackoverflow post

Resources