NodeJS async API Error, when querying embedded array of models - node.js

Thanks for taking the time to read this.
I am currently getting this error in my API endpoint for node express when calling this async endpoint.
(node:32308) UnhandledPromiseRejectionWarning: ValidationError: Conversation validation failed: participants.0: Path `participants.0` is required., participants.1: Path `participants.1` is required.
at model.Document.invalidate (D:\cliquenext\flutter\stashify\backend\node_modules\mongoose\lib\document.js:2976:32)
at D:\cliquenext\flutter\stashify\backend\node_modules\mongoose\lib\document.js:2765:17
at D:\cliquenext\flutter\stashify\backend\node_modules\mongoose\lib\schematype.js:1334:9
at processTicksAndRejections (internal/process/task_queues.js:79:11)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:32308) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:32308) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Basically I am trying to get a conversation, with its messages and its participants and return it.
The conversation object model is like so:
const conversationSchema = new Schema(
{
isGroup: {
type: Boolean,
required: true,
},
creator: {
type: Schema.Types.ObjectId,
ref: 'User',
},
passwordProtected: {
type: Boolean,
default: false,
},
password: {
type: String,
},
participants: [
{
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
},
],
deleted: [
{
type: Schema.Types.ObjectId,
ref: 'User',
},
],
seen: [
{
type: Schema.Types.ObjectId,
ref: 'User',
},
],
messages: [
{
type: Schema.Types.ObjectId,
ref: 'Message',
},
],
},
{ timestamps: true },
);
and this is my endpoint:
exports.getMessagesFromUser = async (req, res, next) => {
try {
// variables
const senderId = req.userId;
const receiverId = req.body.receiverId;
// get users
const sender = await User.findOne({ _id: senderId });
const receiver = await User.findOne({ _id: receiverId });
const ids = [senderId, receiverId];
// look for an existing conversation between two users
// where the participants include both users
// where the conversation is a single conversation
// that the sender hasn't deleted
// if sender chooses to send a message, any undeleted conversations will be resurrected
let conversation = await Conversation.findOne({
isGroup: false,
participants: {
$all: ids,
},
})
.select('participants deleted seen messages')
.populate({ path: 'messages', select: 'sender message deleted seen createdAt' })
.populate({ path: 'participants', select: '_id' })
.populate({ path: 'participants', select: 'nickname' });
if (conversation) {
// conversation exists
let senderBlocked = false;
let receiverBlocked = false;
let conversationDeleted = false;
let filter;
// check if sender has blocked the receiver
filter = sender.blocked.filter((u) => u._id == receiverId);
if (filter.length > 0) {
senderBlocked = true;
}
filter = null;
// check if receiver has blocked the sender
filter = receiver.blocked.filter((u) => u._id == senderId);
if (filter.length > 0) {
receiverBlocked = true;
}
filter = null;
// check if sender has deleted the conversation
filter = conversation.deleted.filter((u) => u._id == senderId);
if (filter.length > 0) {
conversationDeleted = true;
}
// get, map and return the messageSchema
res.status(200).json({
message: 'Conversations fetched successfully!',
conversation: conversation,
senderBlocked: senderBlocked,
receiverBlocked: receiverBlocked,
conversationDeleted: conversationDeleted,
});
} else {
// if conversation does not exist, return empty array
// conversations will be created upon initial message between users
res.status(200).json({
message: 'No Conversations Yet.',
conversation: [],
senderBlocked: false,
receiverBlocked: false,
conversationDeleted: false,
});
}
} catch (e) {
e.statusCode = 500;
next(e);
}
};
I was able to determine the error was throwing inside this endpoint but couldn't for the life of me figure out why. I have added a try, catch and next block. Made sure I had the appropriate awaits in order, etc. My only guess here is the mongoose query of populate and select is wrong somehow? What do you think I am doing wrong here? Please and thank you.

As jimmy pointed out, the problem wasn't there. it was in another socket route where I was trying to save new conversations if they didnt exist.
So I had something like:
socket.on('join-inbox-rooms', async (data) => {
// decode json string
const decodedData = await JSON.parse(data);
// get conversation
let conversation = await Conversation.findOne({
isGroup: false,
participants: {
$all: [decodedData.senderId, decodedData.receiverId],
},
});
if (!conversation) {
conversation = new Conversation({
isGroup: false,
creator: decodedData.senderId,
passwordProtected: false,
paassword: null,
participants: [
decodedData.senderId,
decodedData.receiverId,
],
deleted: [],
seen: [],
messages: [],
});
await conversation.save();
}
Now I was getting the ids from the front end as strings. They should be mongo objects.
I ended up fixing it like so:
const { ObjectId } = require('mongodb');
if (!conversation) {
conversation = new Conversation({
isGroup: false,
creator: decodedData.senderId,
passwordProtected: false,
paassword: null,
participants: [
ObjectId(decodedData.senderId),
ObjectId(decodedData.receiverId),
],
deleted: [],
seen: [],
messages: [],
});
await conversation.save();
}

Related

Maching ids doesn't return true when compared

I'm trying to make some comprobations on my API (Node.js + Mongodb)
I want to check if the proposerId is equal to the eventOrganizer. To do so
I'm sending this on the body:
{
"participantId": "6238a608170aff10d16ccd89",
"proposerId": "62385d8caee17d13a1762b39", // this guy id is also an organizer
"gender": "female",
"groupId": "623aea21fcfad83bcf8d5bc4"
}
in my PATCH controller to add a user I have this verification:
exports.addParticipantToEvent = async (req, res, next) => {
// Group Organizer constants
const eventId = req.params.eventId;
const groupId = req.body.groupId;
const proposerId = req.body.proposerId; // it can be an admin adding someone to the participants list
// Participant constants
const participantId = req.body.participantId;
const gender = req.body.gender;
// EVENT
// Does the event even exist?
const eventData = await Event.findById(eventId);
if (!eventData) {
return res.status(406).json({
code: 'EVENT_DOESNT_EXIST',
message: 'The event is not valid.',
});
}
console.log(eventData);
// What kind of users can participate in this event?
const allowedParticipants = eventData.allowedParticipants;
// whos the event organizer?
const eventOrganizer = eventData.organizer._id;
console.log('Organizer: ' + eventOrganizer);
console.log('Proposer: ' + proposerId);
console.log('Result: ' + proposerId === eventOrganizer);
try {
return res.status(200).json({
message: 'The participant can be added',
participantId: participantId,
gender: gender,
allowedParticipants: allowedParticipants,
});
} catch (err) {
return res.status(400).json({ message: err });
}
};
I want to verify is the proposerId is an admin or an organizer of the event, so I console.log the eventData entirely and later I console log all the constants I want to verify and the result, it gives me false all the time.
Maybe I need to specify better something?
{
location: { province: 'Barcelona' },
_id: new ObjectId("634ffee75990124926431e6f"),
title: 'Test open close 14',
sport: new ObjectId("622ce6ca672c3d4447676705"),
group: new ObjectId("623aea21fcfad83bcf8d5bc4"),
organizer: new ObjectId("62385d8caee17d13a1762b39"),
participants: [ new ObjectId("62385d8caee17d13a1762b39") ],
replacements: [],
invitations: [],
when: 2022-10-09T13:43:02.999Z,
open: true,
costPerParticipant: 4.4,
skill: 'novice',
allowedGender: 'female',
minParticipants: 2,
maxParticipants: 5,
visible: false,
externalLink: 'https://www.komoot.es/tour/731122050?ref=wta',
allowInvitations: true,
allowReplacements: false,
allowedParticipants: 'only-members',
createdAt: 2022-10-19T13:43:03.006Z,
updatedAt: 2022-10-19T13:43:03.006Z,
__v: 0
}
Organizer: 62385d8caee17d13a1762b39
Proposer: 62385d8caee17d13a1762b39
false
As you can see, both Organizer and proposer are same id, yet I get false.
After lurking some more, I have found that to validate mongos object ids with strings I need to use equals(). So now I have the solution.

Update multiple documents instance in MongoDb collection with Mongoose. save() is not a function

Mongoose newbe here. I got the following function to update the references (deleting them) in the document Post when a Tag is deleted. When I call my GraphQl API this is what I got:
message": "posts.save is not a function"
The function in my gql resolver:
async deleteTag(root, { id }, context) {
const posts = await Post.find();
const tag = await Tag.findById(id);
if(!tag){
const error = new Error('Tag not found!');
error.code = 404;
throw error;
}
posts?.forEach(async (post) => {
await post.tags.pull(id);
})
await posts.save()
await Tag.findByIdAndRemove(id);
return true;
}
This is the Post model:
const PostSchema = new Schema({
body: {
type: String,
required: true
},
tags: {
type: [Schema.Types.ObjectId],
ref: 'Tag',
required: false
},
});
and this is the Tag model:
const TagSchema = new Schema(
{
name: {
type: String,
required: true
},
},
{ timestamps: true }
);
Looks like I can't call the method save() on the array of objects returned by Exercise.find()
I used the same pattern in other functions, the difference is that there I used .findById()
Any solution? Advice and best practice advide are super welcome.
You have to save the posts individually:
posts?.forEach(async (post) => {
await post.tags.pull(id);
await post.save();
})
Or use Model.updateMany() combined with the $pull operator.
FWIW, you should probably limit the number of matching Post documents by selecting only documents that have the specific tag listed:
await Post.find({ 'tags._id' : id });

Mongoose | Middleware | Rollback operations performed by pre/post hooks when an error is thrown

Considering the code below, how can a transaction be implemented in order to ensure that someParentDocument doesn't get deleted and any operations performed inside the hooks are rolledback, when an error is thrown in any of the hooks?
const parentSchema = new mongoose.Schema({
name: String,
children: [{ type: mongoose.Schema.Types.ObjectId, ref: "Child" }],
});
const childSchema = new mongoose.Schema({
name: String,
parent: { type: mongoose.Schema.Types.ObjectId, ref: "Parent" },
});
parentSchema.pre("remove", async function(next){
// Add and remove documents to Parent and Child...
//...
next();
});
parentSchema.post("remove", async function(parent){
throw new Error("Exception!");
});
// (...)
try {
await someParentDocument.delete(); // <-- will trigger the hooks above
} catch {}
TLDR; Mongoose middleware was not designed for this.
This method of inserting transactions is actually patching the middleware functionality, and you are essentially creating an api completely separate from the mongoose middleware.
What would be better is inverting the logic for your remove query in a separate function.
Simple & Intended Solution
Allow a transaction handling method to do its magic, and create a separate remove method for your parent model. Mongoose wraps mongodb.ClientSession.prototype.withTransaction with mongoose.Connection.prototype.transaction and we don't even have to instantiate or manage a session! Look at the different between the length of this and that below. And you save the mental headache of remembering the internals of that middleware at the cost of one separate function.
const parentSchema = new mongoose.Schema({
name: String,
children: [{ type: mongoose.Schema.Types.ObjectId, ref: "Child" }],
});
const childSchema = new mongoose.Schema({
name: String,
parent: { type: mongoose.Schema.Types.ObjectId, ref: "Parent" },
});
// Assume `parent` is a parent document here
async function fullRemoveParent(parent) {
// The document's connection
const db = parent.db;
// This handles everything with the transaction for us, including retries
// session, commits, aborts, etc.
await db.transaction(async function (session) {
// Make sure to associate all actions with the session
await parent.remove({ session });
await db
.model("Child")
.deleteMany({ _id: { $in: parent.children } })
.session(session);
});
// And done!
}
Small Extension
Another way to make this easy, is to register a middleware which simply inherits a session iff_ the query has one registered. Maybe throw an error if a transaction has not been started.
const parentSchema = new mongoose.Schema({
name: String,
children: [{ type: mongoose.Schema.Types.ObjectId, ref: "Child" }],
});
const childSchema = new mongoose.Schema({
name: String,
parent: { type: mongoose.Schema.Types.ObjectId, ref: "Parent" },
});
parentSchema.pre("remove", async function () {
// Look how easy!! Just make sure to pass a transactional
// session to the removal
await this.db
.model("Child")
.deleteMany({ _id: { $in: parent.children } })
.session(this.$session());
// // If you want to: throw an error/warning if you forgot to add a session
// // and transaction
// if(!this.$session() || !this.$session().inTransaction()) {
// throw new Error("HEY YOU FORGOT A TRANSACTION.");
// }
});
// Assume `parent` is a parent document here
async function fullRemoveParent(parent) {
db.transaction(async function(session) {
await parent.remove({ session });
});
}
Risky & Complex Solution
This works, and is totally, horribly complex. Not recommended. Will likely break some day because it relies on intricacies of the mongoose API. I don't know why I coded this, please don't include it in your projects.
import mongoose from "mongoose";
import mongodb from "mongodb";
const parentSchema = new mongoose.Schema({
name: String,
children: [{ type: mongoose.Schema.Types.ObjectId, ref: "Child" }],
});
const childSchema = new mongoose.Schema({
name: String,
parent: { type: mongoose.Schema.Types.ObjectId, ref: "Parent" },
});
// Choose a transaction timeout
const TRANSACTION_TIMEOUT = 120000; // milliseconds
// No need for next() callback if using an async function.
parentSchema.pre("remove", async function () {
// `this` refers to the document, not the query
let session = this.$session();
// Check if this op is already part of a session, and start one if not.
if (!session) {
// `this.db` refers to the documents's connection.
session = await this.db.startSession();
// Set the document's associated session.
this.$session(session);
// Note if you created the session, so post can clean it up.
this.$locals.localSession = true;
//
}
// Check if already in transaction.
if (!session.inTransaction()) {
await session.startTransaction();
// Note if you created transaction.
this.$locals.localTransaction = true;
// If you want a timeout
this.$locals.startTime = new Date();
}
// Let's assume that we need to remove all parent references in the
// children. (just add session-associated ops to extend this)
await this.db
.model("Child") // Child model of this connection
.updateMany(
{ _id: { $in: this.children } },
{ $unset: { parent: true } }
)
.session(session);
});
parentSchema.post("remove", async function (parent) {
if (this.$locals.localTransaction) {
// Here, there may be an error when we commit, so we need to check if it
// is a 'retryable' error, then retry if so.
try {
await this.$session().commitTransaction();
} catch (err) {
if (
err instanceof mongodb.MongoError &&
err.hasErrorLabel("TransientTransactionError") &&
new Date() - this.$locals.startTime < TRANSACTION_TIMEOUT
) {
await parent.remove({ session: this.$session() });
} else {
throw err;
}
}
}
if (this.$locals.localSession) {
await this.$session().endSession();
this.$session(null);
}
});
// Specific error handling middleware if its really time to abort (clean up
// the injections)
parentSchema.post("remove", async function (err, doc, next) {
if (this.$locals.localTransaction) {
await this.$session().abortTransaction();
}
if (this.$locals.localSession) {
await this.$session().endSession();
this.$session(null);
}
next(err);
});

Blank document and unhandled promise rejection when making an add subdocument request

I am working on my first request that would add a subdocument to a document in MongoDB and I'm struggling with it. My database is a collection of users, and each user has an array of words they working on learning to translate in the application I am building. I am currently having two issues
when I make a request in postman to add a new word to my user's array of words, I add a new object that has ID, but none of the other property value pairs that I have in the word sub-model, and in the request(greek, english, success, timestamp).
my command prompt gives me the following errors
(node:8320) UnhandledPromiseRejectionWarning: Unhandled promise
rejection. This error originated either by throwing inside of an async
function without a catch block, or by rejecting a promise which was
not handled with .catch(). (rejection id: 3) (node:8320)
UnhandledPromiseRejectionWarning: ValidationError: user validation
failed: words.0.greek: Path greek is required., words.0.english:
Path english is required., words.0.success: Path success is
required., words.0.timeStamp: Path timeStamp is required.
The second error is confusing because in my mind it should be word.greek and words.english to get the value from the each word object. However, it adds a 0 between the object and its property/value pair.
My mongoose model for the subdocument is as follows
const wordSchema = new Schema({
greek: {
type: String,
required: true,
trim: true,
minlength: 1,
index: { unique: true, sparse: true },
},
english: {
type: String,
required: true,
minlength: 1
},
success: {
type: Number,
required: true
},
timeStamp: {
type: Date,
required: true
},
});
This is my request to add the word to the User's array of words.
router.post("/update/:id",(req, res) =>{
console.log(req.body.greek)
var greek = req.body.greek;
var english = req.body.english;
var success = req.body.success;
var timeStamp = req.body.timeStamp
var newWord = {
greek: greek,
english: english,
success: success,
timeStamp: timeStamp
}
User.findById(req.params.id)
.then((user) => {
user.words.push(newWord);
user.save()
res.status(200).json(user)
.catch((err) => {res.status(400).json(err)})
})
.catch((err) => {res.status(400).json("Error: "+err)})
});
Any help would be greatly appreciated! I've done some googling on adding subdocuments to a document, but I still haven't found the solution.
Instead of fetching and then updating the document you can directly update the document in one DB call.
router.post("/update/:id",(req, res) =>{
console.log(req.body.greek)
var greek = req.body.greek;
var english = req.body.english;
var success = req.body.success;
var timeStamp = req.body.timeStamp
var newWord = {
greek: greek,
english: english,
success: success,
timeStamp: timeStamp
}
User.findOneAndUpdate({ _id: req.params.id }, {
$push: {
words: newWord
}
})
.then((user) => {
if(!user){
return res.status(404).json({
message: 'Not User matches given ID'
});
}
res.status(200).json(user);
})
.catch((err) => {res.status(400).json("Error: "+err)})
});
one thing I see is user.save() will return a promise, which you do not handle, hence the document will not be save. maybe consider:
User.findById(req.params.id)
.then(async (user) => {
user.words.push(newWord);
await user.save()
res.status(200).json(user)
.catch((err) => {res.status(400).json(err)})
})
.catch((err) => {res.status(400).json("Error: "+err)})

Mongoose Subdocument will not update

I am having trouble figuring out if I designed the schema correctly because I am receiving a 500 error when attempting to PATCH changes of the roles property from a profile. (Note: The 500 error just responds with an empty {}, so it isn't really informative)
Below is the profile schema:
var ProfileSchema = new Schema({
name: {
type: String,
required: true
},
roles: [{
application: {
type: Schema.Types.ObjectId,
required: true,
ref: 'Application'
},
role: {
type: String,
required: true,
enum: [ 'admin', 'author', 'publisher' ]
}
}]
});
Each profile has a role for an application, and when I send the request to the controller action 'update', it fails:
profile update controller:
// Updates an existing Profile in the DB
export function update(req, res) {
try {
if (req.body._id) {
delete req.body._id;
}
console.log('ENDPOINT HIT...');
console.log(`REQUEST PARAM ID: ${req.params.id}`);
console.log('REQUEST BODY:');
console.log(req.body);
console.log('ENTIRE REQUEST: ');
return Profile.findByIdAsync(req.params.id)
.then(handleEntityNotFound(res))
.then(saveUpdates(req.body))
.then(respondWithResult(res))
.catch(handleError(res));
} catch(ex) {
console.error('FAILED TO UPDATE PROFILE');
return handleError(res);
}
}
I made sure that the id and body was being sent properly, and I am hitting the end point.
This is an example of the request body JSON:
{
_id: 57e58ad2781fd340563e29ff,
__updated: Thu Oct 27 2016 15:41:12 GMT-0400 (EDT),
__created: Fri Sep 23 2016 16:04:34 GMT-0400 (EDT),
name: 'test',
__v: 11,
roles:[
{ application: 57b70937c4b9fe460a235375,
role: 'admin',
_id: 58125858a36bd76d8111ba16 },
{ application: 581b299f0145b48adf8f57bd,
role: 'publisher',
_id: 581b481898eefb19ed8a73ee }
]
}
When I try to find the Profile by Id, the promise chain goes straight to the catch(handleError(res)); part of the code and shows an empty object in my console.
My handle error function:
function handleError(res, statusCode) {
console.error('HANDLE PROFILE ERROR: ', statusCode);
statusCode = statusCode || 500;
return function(err) {
console.error('PROFILE ERROR:');
console.error(JSON.stringify(err, null, 2));
res.status(statusCode).send(err);
};
}
UPDATE
I am realizing the code is breaking when it hits my saveUpdates function (Note: I am using lodash):
function saveUpdates(updates) {
/// the code is fine here ///
return function(entity) {
/// once it enters in here, is where it begins to break ///
var updated = _.merge(entity, updates);
if(updated.roles.length != updates.roles.length) {
updated.roles = updates.roles;
}
for(var i in updates.roles) {
updated.roles.set(i, updates.roles[i]);
}
return updated.saveAsync()
.then(updated => {
return updated;
});
};
}
Lesson learned: Read Documentation properly.
Since I am using bluebird promises for this application, I forgot to use .spread() within my saveUpdates() callback function.
Solution:
function saveUpdates(updates) {
return function(entity) {
var updated = _.merge(entity, updates);
if(updated.roles.length != updates.roles.length) {
updated.roles = updates.roles;
}
for(var i in updates.roles) {
updated.roles.set(i, updates.roles[i]);
}
return updated.saveAsync()
// use `.spread()` and not `.then()` //
.spread(updated => {
return updated;
});
};
}
I want to thank the following SOA that led to this conclusion: https://stackoverflow.com/a/25799076/5994486
Also, here is the link to the bluebird documentation in case anyone was curious on .spread(): http://bluebirdjs.com/docs/api/spread.html

Resources