Mongoose Node.JS check for duplicate items in database array - node.js

I am creating a messaging application and I am looking for a way to check to see if there already exists a conversation between people before a new one is created. Here is the implementation I currently have:
export const createConversation = async (req, res) => {
const conversation = req.body.conversation;
const message = req.body.message;
try {
//Check if conversation between participants exists
const newConversation = await Conversation.find({participants: conversation.participants});
if(newConversation != null) {
newConversation = new Conversation(conversation);
}
message.conversationId = newConversation._id;
const newMessage = new Messages(message);
await newConversation.save();
await newMessage.save();
res.status(201).json({conversation: newConversation, message: newMessage});
} catch (error) {
res.status(409).json({ message: error.message });
}
};
I want to check if there exists a conversation in MongoDB atlas where the participant list contains the same Ids as the conversation that is trying to be created. What is the most efficient way to implement this?

Related

Sequelize not retrieving all data after insert

I have noticed that my backend is not retrieving the expected data after an insert.
In my React application, I have one function which inserts data into the database and after getting a response, a new request is sent to update the current component state with the newly fetched data.
All my functions are using await/async and in the backend, all transactions are correctly used and committed in order.
My client is calling the following endpoints:
-POST: api/ticket ( INSERT AN ITEM)
-GET: api/ticket (GET ALL ITEMS)
Here is what the backend is showing which looks correct to me, the problem is that in the 'SELECT' statement, the inserted item is not retrieved.
The transactions are started from two different routes but I don't see why it should be an issue.
In addition, I tried to change the AddItem function to output the same findAll statement which is called when using the GET method and the data returned are correct.
So why if I separate these two flows I do not get all the items? I always need to refresh the page to get the added item.
START TRANSACTION;
Executing (a9d14d5c-c0ac-4821-9b88-293b086debaa): INSERT INTO `messages` (`id`,`message`,`createdAt`,`updatedAt`,`ticketId`,`userId`) VALUES (DEFAULT,?,?,?,?,?);
Executing (a9d14d5c-c0ac-4821-9b88-293b086debaa): COMMIT;
Executing (9ee9ddaa-294e-41d1-9e03-9f02a2737030): START TRANSACTION;
Executing (9ee9ddaa-294e-41d1-9e03-9f02a2737030): SELECT `ticket`.`id`, `ticket`.`subject`, `ticket`.`status`, `ticket`.`createdAt`, `ticket`.`updatedAt`, `ticket`.`deletedAt`, `ticket`.`userId`, `messages`.`id` AS `messages.id`, `messages`.`message` AS `messages.message`, `messages`.`sender` AS `messages.sender`, `messages`.`createdAt` AS `messages.createdAt`, `messages`.`updatedAt` AS `messages.updatedAt`, `messages`.`deletedAt` AS `messages.deletedAt`, `messages`.`ticketId` AS `messages.ticketId`, `messages`.`userId` AS `messages.userId`, `messages->user`.`id` AS `messages.user.id`, `messages->user`.`firstname` AS `messages.user.firstname`, `messages->user`.`surname` AS `messages.user.surname`, `messages->user`.`email` AS `messages.user.email`, `messages->user`.`password` AS `messages.user.password`, `messages->user`.`stripeId` AS `messages.user.stripeId`, `messages->user`.`token` AS `messages.user.token`, `messages->user`.`birthDate` AS `messages.user.birthDate`, `messages->user`.`status` AS `messages.user.status`, `messages->user`.`confirmationCode` AS `messages.user.confirmationCode`, `messages->user`.`createdAt` AS `messages.user.createdAt`, `messages->user`.`updatedAt` AS `messages.user.updatedAt`, `messages->user`.`deletedAt` AS `messages.user.deletedAt` FROM `tickets` AS `ticket` LEFT OUTER JOIN `messages` AS `messages` ON `ticket`.`id` = `messages`.`ticketId` AND (`messages`.`deletedAt` IS NULL) LEFT OUTER JOIN `users` AS `messages->user` ON `messages`.`userId` = `messages->user`.`id` AND (`messages->user`.`deletedAt` IS NULL) WHERE (`ticket`.`deletedAt` IS NULL);
Executing (9ee9ddaa-294e-41d1-9e03-9f02a2737030): COMMIT;
-- POST '/api/ticket
exports.addMessage = async (req, res) => {
try {
const result = await sequelize.transaction(async (t) => {
var ticketId = req.body.ticketId;
const userId = req.body.userId;
const message = req.body.message;
const subject = req.body.subject;
// Validate input - If new ticket, a subject must be provided
if (!ticketId && !subject) {
return res
.status(400)
.send({ message: "New ticket must have a subject" });
}
// Validate input - If ticket exists, userId and message must be provided
if (!userId && !message && ticketId) {
return res
.status(400)
.send({ message: "UserID and message are required" });
}
// Create ticket is no ticketID was provided
if (!ticketId) {
const [ticket, created] = await Ticket.findOrCreate({
where: {
subject: subject,
userId: userId,
},
transaction: t,
});
ticketId = ticket.id;
}
// Create a new message object
const messageObject = await db.message.create(
{
message: message,
userId: userId,
ticketId: ticketId,
},
{ transaction: t }
);
// Output message object
return res.send(messageObject);
});
} catch (err) {
console.log(err);
return res.status(500).send({
message:
err.message || "Some error occurred while creating the ticket message.",
});
}
};
-- GET: api/ticket
exports.findAll = async (req, res) => {
try {
const result = await sequelize.transaction(async (t) => {
const tickets = await db.ticket.findAll(
{
include: [{ model: db.message, include: [db.user] }],
},
{ transaction: t }
);
tickets.forEach((ticket) => {
console.log(JSON.stringify(ticket.messages.length));
});
return res.send(tickets);
});
} catch (err) {
console.log(err);
res.status(500).send({
message: err.message || "Some error occurred while retrieving Tickets.",
});
}
};
You sent a response to a client before the transaction actually was committed. You just need to move res.send(messageObject); outside the transaction call.
You can try to look what's going on in the current version of your code if you add several console.log with messages to see what the actual order of actions is (I mean a couple of messages in POST (the last statement inside transaction and after transaction before res.send) and at least one at the beginning of GET).
Actually if the transaction was rolled back you'd send an uncommited and already removed object/record that I suppose is not your goal.

firebase cloud functions iterate over collection

I am building a forum, and when a person comments on a thread, I would like to email everyone that someone has added a comment. This requires iterating over a firestore collection. How do I do this using firebase cloud functions?
exports.onCommentCreation = functions.firestore.document('/forum/threads/threads/{threadId}/comments/{commentId}')
.onCreate(async(snapshot, context) => {
var commentDataSnap = snapshot;
var threadId = context.params.threadId;
var commentId = context.params.commentId;
// call to admin.firestore().collection does not exist
var comments = await admin.firestore().collection('/forum/threads/threads/{threadId}/comments/');
// iterate over collection
});
You need to run get query, not sure what is admin here, but you can do like this:
const citiesRef = db.collection('cities'); // pass your collection name
const snapshot = await citiesRef.where('capital', '==', true).get(); // query collection
if (snapshot.empty) {
console.log('No matching documents.');
return;
}
snapshot.forEach(doc => {
console.log(doc.id, '=>', doc.data());
});
You can check this link, for more details.
Also, I would suggest you go through this link to correctly set up the cloud function for firestore if there are any issues with it.

How to get userId from Firestore on Node.js backend?

Im new to firebase and I am having trouble setting up the user profile route on my app. Im trying to make it so that the userId stored in Firestore can be retrieved and all the posts made by that specific user will be displayed on the user profile page. Please any help would be appreciated!
This is my Firestore database :
and this is what I have so far but it is not displaying all the posts associated with the user id
const express = require("express");
const router = express.Router();
const firestore = require("firebase/firestore");
const db = firestore.getFirestore();
router.get("/", (req, res) => {
res.send("No user id provided");
});
router.get("/:uid", (req, res) => {
const uid = req.params.uid;
const userPost = firestore.getDoc(firestore.doc(db, "reviews", uid));
userPost(uid, req.query && req.query.dev === "true")
.then((user) => {
return res.send(user);
})
.catch((e) => {
return res.send(e);
});
});
module.exports = router;
This line gets the single (probably non-existant) post at /reviews/:uid:
const userPost = firestore.getDoc(firestore.doc(db, "reviews", uid));
What you are looking to do is build a query for documents in the collection /reviews where docData.userId = uid.
const reviewsColRef = firestore.collection(db, "reviews");
const userPostsQuery = firestore.query(reviewsColRef, firestore.where("userId", "==", uid));
const userPostsPromise = firestore.getDocs(userPostsQuery);
userPostsPromise
.then((querySnapshot) => {
const postsArray = querySnapshot.docs.map(docSnap => {
const docData = { id: docSnap.id, ...docSnap.data() };
delete docData.userId; // it's the same for every post, may as well omit it
delete docData.email; // same as above
return docData;
});
res.json({ posts: postsArray, count: postsArray.length });
})
.catch((err) => {
// don't send the full error object to the client,
// instead you should log the error and send the
// client as little information as possible
console.error(`Failed to get user posts for firebase:${uid}`, err);
res.status(500).json({ error: err.code || err.message }); // Don't forget to use an appropriate status code!
});
Notes:
I recommend using destructuring to import the Firestore lib. Importing it as firestore negates the benefits of the modular SDK of only importing what you need.
import { getFirstore, query, collection, ... } from "firebase/firestore";
If this code is running on a server you control, consider switching to the Firebase Admin SDK instead as this allows you to bypass security rules and has relaxed rate limits.
import { getFirestore, query, collection, ... } from "firebase-admin/firestore";
As a side note, if an import from the SDK conflicts with a name you want to use elsewhere, you can rename it. You might see this when using the RTDB and storage together as they both export ref:
import { getDatabase, ref: rtdbRef } from "firebase/database";
import { getStorage, ref: storageRef } from "firebase/storage";
const userDataRef = rtdbRef(getDatabase(), "users", uid, "data");
const imgStorageRef = storageRef(getStorage(), "path/to/image.png");
Treat emails as sensitive data akin to a phone number. Nobody likes spam and limiting ways to rip emails from your database is good practice. Unless you are working on an internal tool for a corporate network, you should hide the user's email as much as possible.
Email addresses should not be stored with reviews. Instead keep a collection with documents for each user (e.g. document at /userProfile/:uid) and limit access to privileged users.
Consider adding a 404 Not Found error when a user doesn't exist.
Consider adding authentication to restrict access to your API.

How to start a transaction in MongoDb?

I'm trying to prevent concurrent requests to a specific record, see the following example:
function addMoney(orderID,orderID){
const status = Database.getOrder(orderID);
if (status === 1){
return "Money Already Added";
}
Database.udateOrder(orderID, {status: 1});
Database.addMoney(userID, 300);
return true;
}
Assume someone made this request exactly at the same time, therefore the "status" check passed, they'd be able to get Database.addMoney run twice.
Using MySQL, I'd start a transction to lock the row but not sure how to do so using MongoDB.
You can do the transactions in mongodb like MySQL. Consider having an order document with id:123 and status:0. Then you can check for status in a transaction and return if it's already paid or fall through in order to add money document and update order status.
If you face any issue like Transaction numbers are only allowed on a replica set member or mongos this link might help.
In order to use transactions, you need a MongoDB replica set, and
starting a replica set locally for development is an involved process.
The new run-rs npm module makes starting replica sets easy.
const uri = 'mongodb://localhost:27017';
const dbName = 'myDB';
const MongoClient = require('mongodb').MongoClient;
async function main() {
const client = new MongoClient(uri);
await client.connect();
const session = client.startSession();
try {
await session.withTransaction(async () => {
const orders = client.db(dbName).collection('orders');
const money = client.db(dbName).collection('money');
let doc = await orders.findOne({orderID: 123});
if (doc && doc.status === 1) {
console.log("Money Already Added");
return
}
await orders.updateOne({orderID: 123}, {'$set': {status: 1}});
await money.insertOne({orderID: 123, userID: 100, amount: 300}, {session});
console.log("Money added");
});
await session.commitTransaction();
} catch (e) {
console.log(e);
} finally {
await session.endSession();
await client.close();
}
}
main()
The code above may need improvement because I couldn't test it on MongoDB with replica set.

How to read the latest document created in Firestore using node?

I wrote a cloud function, to listen for document creation in a collection, in my database
here is the function,
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().functions);
var newData;
exports.myTrigger = functions.firestore.document('FCM/{id}').onCreate(async (snapshot, context) => {
//
if (snapshot.empty) {
console.log('No Devices');
return;
}
newData = 'hello';
const deviceIdTokens = await admin
.firestore()
.collection('FCM')
.get();
var tokens = [];
var i=0;
for (var token of deviceIdTokens.docs) {
tokens.push(token.data().ar1[i]);
i++;
}
var payload = {
notification: {
title: 'push title',
body: 'push body',
sound: 'default',
},
data: {
push_key: 'Push Key Value',
key1: newData,
},
};
try {
const response = await admin.messaging().sendToDevice(tokens, payload);
console.log('Notification sent successfully');
} catch (err) {
console.log(err);
}
});
This function works weirdly,
For example, sometimes it sends notification, and sometimes it does not.
I don't know how to resolve this issue,
In my arr1 field, i have an array of device tokens, to whom i want to send notifications to,
i want the function to send notifications only to the devices(using tokens) which are just created(in the newly created document ),then delete the document.
I think it's sending notifications to all the documents at once.
I'm pretty new at node..
Is there anyway to only send notifications to the device tokens present in only one document (..the latest created document)?.. I think it's sending notifications to all.
please help me out.
UPDATE:- Here is my document structure
Firstly, in an onCreate handler, you can access the id of just the newly created document by through the snap object, you can access the id via snapshot.id and the body via snapshot.data().
You already have the newly created document fetched, so no need to fetch entire collection. You should replace this entire section:
const deviceIdTokens = await admin
.firestore()
.collection('FCM')
.get();
var tokens = [];
var i=0;
for (var token of deviceIdTokens.docs) {
tokens.push(token.data().ar1[i]);
i++;
}
with this:
const tokens = snapshot.data().ar1;
UPDATE: To delete the new document, you could do
await firestore().collection("FCM").doc(snapshot.id).delete();
since the new document belongs in the FCM collection, not "helpReqs"

Resources