How to elegantly PATCH changes into mongodb without path conflicts - node.js

So I'm attempting to do a PATCH request to a MongoDB store, by updating an element in an sub-array. This is what the data looks like:
const booksSchema = mongoose.Schema(
{
name: { type: String, trim: true },
author: { type: String, trim: true },
},
{ timestamps: true },
)
const Book = mongoose.model('Book', bookSchema)
const librarySchema = mongoose.Schema(
{ books: [ Books.schema ] },
{ timestamps: true },
)
const Library = mongoose.model('Library', librarySchema)
And this is how I'm updating the data:
const library = await Library.findOneAndUpdate(
{ _id: libraryId, "books._id": bookId },
{ $set: { "books.$": bookBody } },
)
And in the process, I've run into problems with the automated timestamp update conflicting with the $set command:
Updating the path 'books.$.updatedAt' would create a conflict at 'books.$'
The best solution I've come across to make this type of update without removing the automated timestamp update is to manually set each field, like this:
const library = await Library.findOneAndUpdate(
{ _id: libraryId, "book._id": bookId },
{
$set: {
"book.$.name": bookBody.name,
"book.$.author": bookBody.author,
}
},
)
...but this solution is very verbosely.
Is there a more elegant way to do this type of PATCH request, perhaps using some kinda of object destructuring, that doesn't require maintaining a mapping of each field?

In fact, if your intention is to perform a PATCH operation the way it's described in HTTP protocol:
The HTTP PATCH request method applies partial modifications to a
resource.
Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH
You should go with the "verbose" solution in your case, because your alternative update { $set: { "books.$": bookBody } } will delete any property missing in the update from the updated book. It's not how PATCH should work, it's how PUT is intended to work.
You can make your code a little less verbose by introducing a helper function performing something like this (example uses lodash):
import _ from 'lodash'
_.mapValues(book, (value) => {
_.mapKeys(value, (innerValue, innerKey) => (
`book.$.${innerKey}`
)
)

Related

MongoDB: add object to subarray if not exists

I searched many questions here and other articles on the web, but they all seem to describe somehow different cases from what I have at hand.
I have User schema:
{
username: { type: String },
lessons: [
{
lesson: { type: String },
result: { type: String }
}
]
}
I want to add new element into lessons or skip, if there is already one with same values, therefore I use addToSet:
const dbUser = await User.findOne({ username })
dbUser.lessons.addToSet({ lesson, result: JSON.stringify(result) })
await dbUser.save()
However it makes what seems to be duplicates:
// first run
[
{
_id: 60c80418f2bcfe5fb8f501c1,
lesson: '60c79d81cf1f57221c05fdac',
result: '{"correct":2,"total":2}'
}
]
// second run
[
{
_id: 60c80418f2bcfe5fb8f501c1,
lesson: '60c79d81cf1f57221c05fdac',
result: '{"correct":2,"total":2}'
},
{
_id: 60c80470f2bcfe5fb8f501c2,
lesson: '60c79d81cf1f57221c05fdac',
result: '{"correct":2,"total":2}'
}
]
At this point I see that it adds _id and thus treats them as different entries (while they are identical).
What is my mistake and what should I do in order to fix it? I can change lessons structure or change query - whatever is easier to implement.
You can create sub-documents avoid _id. Just add _id: false to your subdocument declaration.
const userSchema = new Schema({
username: { type: String },
lessons: [
{
_id: false,
lesson: { type: String },
result: { type: String }
}
]
});
This will prevent the creation of an _id field in your subdoc, and you can add a new element to the lesson or skip it with the addToSet operator as you did.

Update Mongoose Array

so basically I have this and I am trying to update the STATUS part of an array.
However, everything I try does nothing. I have tried findOneAndUpdate also. I am trying to identify the specific item in the array by the number then update the status part of that specific array
(Sorry for formatting, I have no idea how to do that on the site yet ...) (Full code can be found here: https://sourceb.in/0811b5f805)
Code
const ticketObj = {
number: placeholderNumber,
userID: message.author.id,
message: m.content,
status: 'unresolved'
}
let tnumber = parseInt(args[1])
let statuss = "In Progress"
await Mail.updateOne({
"number": tnumber
}, { $set: { "status": statuss } })
Schema
const mongoose = require('mongoose')
const mailSchema = new mongoose.Schema({
guildID: { type: String, required: true },
ticketCount: { type: Number, required: true },
tickets: { type: Array, default: [] }
}, { timestamps: true });
module.exports = mongoose.model('Mail', mailSchema)
You need to use something like Mail.updateOne({"guildID": message.guild.id}, {$set: {`tickets.${tnumber}.status`: statuss}})
or for all objects in array:
Mail.updateOne({"guildID": message.guild.id}, {$set: {'tickets.$[].status': statuss}})
Also, you need to create a schema for the tickets, as it is described in docs:
one important reason to use subdocuments is to create a path where there would otherwise not be one to allow for validation over a group of fields

Change a single value of a subdocument in mongodb

I have a Model called Notes. It has a subdocument requests which holds various documents with values userId, reqType, accepted value( false by default) and noteId of the sender, request and note respectively. When the user hits a certain route I want to keep all the data to be as their previous values, just updating the accepted field to true.
The below code leads to no change in the data or a different iteration leads to erasing all the data other than accepted field and modifying it to true.
How should I do this?
const noteSchema = new mongoose.Schema(
requests: [
{
userId: mongoose.Schema.ObjectId,
noteId: mongoose.Schema.ObjectId,
reqType: String,
accepted: {
type: Boolean,
default: false,
},
},
],
}
)
const Note = mongoose.model('Note', noteSchema)
const note = await Note.findById(req.body.noteId)
await note.updateOne({
requests: {
$elemMatch: {
userId: req.body.userId,
reqType: req.body.reqType,
noteId: req.body.noteId,
},
$set: { "requests.$.accepted": true },
},
})
You do not need to retrieve the document and then update it. Just update it. Use this one:
await Note.updateOne(
{
"requests.userId": req.body.userId,
"requests.reqType": req.body.reqType,
"requests.noteId": req.body.noteId
},
{
$set:
{
"requests.$.accepted":true
}
}
);
I checked, it worked.
With first part mongoose will find the document, with $set it will be updated.

What is the best way to handle querystrings in an express api?

I am a beginner nodejs developer, working on developing an express rest api with optional query params.
For example consider the following schema for a user:
phone: {
countryCode: String,
number: phoneType
},
phoneVerified: { type: Boolean, default: false },
emailVerified: { type: Boolean, default: false },
rating: Number,
balance: { type: Number, default: 0 },
occupation: String,
gender: { type: String, enum: genders },
I want to expose this resource at /users and allow querying through optional query strings.
For ex, /users?emailVerified=true&phoneverified=false&gender=male&occupation=plumber&limit=10
This should return all the users which satisfy the criteria, while keeping almost all of the options optional.
What is the best way to do this in a maintenable and futureproof way?
Appraoch 1: My first approach was to use if blocks to check which parameters exist in the query and build mongoose queries accordingly, but that looks ugly and very hard to read.
queryObj = {};
if (req.query.occupation) {
queryObject = {
...queryObject,
occupation: req.query.occuption
};
}
if (req.query.phoneVerified) {
queryObject = {
...queryObject,
phoneVerified: req.query.phoneVerifed
};
}
const users = await User.find(queryObject);
I also found the querymen package which looks promising. If someone experinced could guide me as to what is the best practice? Thanks in advance
you are doing it in right way. you can also try
queryObject = {};
if (req.query.params.occupation) {
queryObject.occupation= req.params.occuption
}
if (req.params.phoneVerified) {
queryObject.phoneVerified= req.params.phoneVerifed
}
const users = await User.find(queryObject);
you can add new properties to json using "."

mongoose subdocument sorting

I have an article schema that has a subdocument comments which contains all the comments i got for this particular article.
What i want to do is select an article by id, populate its author field and also the author field in comments. Then sort the comments subdocument by date.
the article schema:
var articleSchema = new Schema({
title: { type: String, default: '', trim: true },
body: { type: String, default: '', trim: true },
author: { type: Schema.ObjectId, ref: 'User' },
comments: [{
body: { type: String, default: '' },
author: { type: Schema.ObjectId, ref: 'User' },
created_at: { type : Date, default : Date.now, get: getCreatedAtDate }
}],
tags: { type: [], get: getTags, set: setTags },
image: {
cdnUri: String,
files: []
},
created_at: { type : Date, default : Date.now, get: getCreatedAtDate }
});
static method on article schema: (i would love to sort the comments here, can i do that?)
load: function (id, cb) {
this.findOne({ _id: id })
.populate('author', 'email profile')
.populate('comments.author')
.exec(cb);
},
I have to sort it elsewhere:
exports.load = function (req, res, next, id) {
var User = require('../models/User');
Article.load(id, function (err, article) {
var sorted = article.toObject({ getters: true });
sorted.comments = _.sortBy(sorted.comments, 'created_at').reverse();
req.article = sorted;
next();
});
};
I call toObject to convert the document to javascript object, i can keep my getters / virtuals, but what about methods??
Anyways, i do the sorting logic on the plain object and done.
I am quite sure there is a lot better way of doing this, please let me know.
I could have written this out as a few things, but on consideration "getting the mongoose objects back" seems to be the main consideration.
So there are various things you "could" do. But since you are "populating references" into an Object and then wanting to alter the order of objects in an array there really is only one way to fix this once and for all.
Fix the data in order as you create it
If you want your "comments" array sorted by the date they are "created_at" this even breaks down into multiple possibilities:
It "should" have been added to in "insertion" order, so the "latest" is last as you note, but you can also "modify" this in recent ( past couple of years now ) versions of MongoDB with $position as a modifier to $push :
Article.update(
{ "_id": articleId },
{
"$push": { "comments": { "$each": [newComment], "$position": 0 } }
},
function(err,result) {
// other work in here
}
);
This "prepends" the array element to the existing array at the "first" (0) index so it is always at the front.
Failing using "positional" updates for logical reasons or just where you "want to be sure", then there has been around for an even "longer" time the $sort modifier to $push :
Article.update(
{ "_id": articleId },
{
"$push": {
"comments": {
"$each": [newComment],
"$sort": { "$created_at": -1 }
}
}
},
function(err,result) {
// other work in here
}
);
And that will "sort" on the property of the array elements documents that contains the specified value on each modification. You can even do:
Article.update(
{ },
{
"$push": {
"comments": {
"$each": [],
"$sort": { "$created_at": -1 }
}
}
},
{ "multi": true },
function(err,result) {
// other work in here
}
);
And that will sort every "comments" array in your entire collection by the specified field in one hit.
Other solutions are possible using either .aggregate() to sort the array and/or "re-casting" to mongoose objects after you have done that operation or after doing your own .sort() on the plain object.
Both of these really involve creating a separate model object and "schema" with the embedded items including the "referenced" information. So you could work upon those lines, but it seems to be unnecessary overhead when you could just sort the data to you "most needed" means in the first place.
The alternate is to make sure that fields like "virtuals" always "serialize" into an object format with .toObject() on call and just live with the fact that all the methods are gone now and work with the properties as presented.
The last is a "sane" approach, but if what you typically use is "created_at" order, then it makes much more sense to "store" your data that way with every operation so when you "retrieve" it, it stays in the order that you are going to use.
You could also use JavaScript's native Array sort method after you've retrieved and populated the results:
// Convert the mongoose doc into a 'vanilla' Array:
const articles = yourArticleDocs.toObject();
articles.comments.sort((a, b) => {
const aDate = new Date(a.updated_at);
const bDate = new Date(b.updated_at);
if (aDate < bDate) return -1;
if (aDate > bDate) return 1;
return 0;
});
As of the current release of MongoDB you must sort the array after database retrieval. But this is easy to do in one line using _.sortBy() from Lodash.
https://lodash.com/docs/4.17.15#sortBy
comments = _.sortBy(sorted.comments, 'created_at').reverse();

Resources