Mongoose Caching a Part of a Referenced Subdoc on Creation - mongodb

I have a simple schema with references. But I dont want to populate the referenced document on every query. Instead I want to cache a part (only a single attribute) on the referencing document.
A simple example schema:
User
- displayName
(....stuff....)
Posts
- title
- content
- user (reference?)
When I used references and population on demand my PostSchema would look like:
var PostSchema = new Schema({
...
user: {
type: Schema.ObjectId,
ref: 'User'
}
})
But I want to save (cache) only the displayName (not the other stuff in User) and the _id in Post when it is created. My thougts on that is to change the PostSchema as following:
user: {
_id: {
type: Schema.ObjectId,
ref: 'User'
},
displayName: String
}
on the creation of a post I do:
var post = new Post(req.body);
post.user._id = req.user._id;
post.user.displayName = req.user.displayName;
The problem with this is: It looks like now _id is the referenced user, leading to absurd user._id._id. Also my isCreator-middleware now needs to convert the _id to a string:
exports.isCreator = function(req, res, next) {
if (req.post.user._id.toString() !== req.user._id.toString()) {
return res.status(403).send('User is not authorized');
}
next();
};
This cannot be the best solution. So my question is: What is best practice for this case?

Related

Express.js PUT Request not updating MongoDB

I'm working on a project that is basically a super watered down social media website.
I have a chunk done already, but I'm having some issues creating a put request to my mongodb. Basically, I want to send a put request to update a numeric value to be able to have a like counter on each post.
What I'm trying do here is send a put request with a specific post id. I'm storing the post id in a hidden text box to reference it. This is pug formatted HTML:
input.form-control(type='hidden' value=item.id id='postId' placeholder='' name='postId' required='false')
form(method='PUT' action='/update/{{post._id}}')
button(type='submit') Like
Then in my router.js file I'm basically trying to take in that id and set the likes field in the Post schema to 1 (just for testing).
router.put('/update/:id', function (req, res, next) {
let id = {
_id: ObjectID(req.params.id)
};
Post.update({_id: id}, {$set:{'likes': 1}}, (err, result) => {
if(err) {
throw err;
}
res.send('user updated sucessfully');
});
});
Here is my post schema
var mongoose = require("mongoose");
var PostSchema = new mongoose.Schema({
postText: {
type: String,
unique: false,
required: true,
trim: true
},
usernameText: {
type: String,
unique: false,
required: true,
trim: true
},
likes:{
type: Number,
unique: false,
required: false
}
});
var Post = mongoose.model("Posts", PostSchema);
module.exports = Post;
Any and all help would be highly appreciated, thank you
You can't change the ObjectId. The ObjectId is generated by MongoDB and can't be changed by the user using query functions.
If you want to assign a unique id to each user for example, then create a separate field in your schema.
You cannot change the ID
'PUT' method is not supported directly as far as I know. You need method override

how to multi ref in mongoose

I am trying to ref two documents in one property, i have been checking the oficial documentation but i didn't get the solution...
At the moment i am trying this...
items: [{
type: mongoose.Schema.Types.ObjectId,
ref: ['items','users']
}],
In the documentation they mention refPath... but i could not populate both models... any solution for this?
// LINK TO DOCUMENTATION
https://mongoosejs.com/docs/populate.html#dynamic-ref
You don't need to pass refs in arrays. Here is the simple solution:
Mongoose Model (Report.js):
You can clearly see that I did not pass any ref to my Model but still, you can use multiple refs in post/get APIs. I will show you next.
const mongoose = require('mongoose');
const reportSchema = new mongoose.Schema({
reportFrom : {
type: mongoose.Schema.Types.ObjectId,
require: true,
},
reportTo: {
type: mongoose.Schema.Types.ObjectId,
require: true,
},
}
);
module.exports = mongoose.model("report", reportSchema);
Above "reportTo" means the Id of someone post whom the user is going to report or the id of user profile whom the user is going to report. Means "reportTo" may be an ID of User Profile or Post. So, if "reportTo" contains user Id then I have to refer to users collection but if "reportTo" contains post Id then I have to refer to posts collection. So, how I can use two refs. I will simply pass type query from postman to tell which ref to go either posts or users. See below my API request:
APIs file (reports.js)
const reports = req.query.type === "Post" ? await Report.find({reportTo: req.params.id}).populate({
path: 'reportFrom', // attribute name of Model
model: "User", // name of model from where you want to populate
select: "name profilePicture", // get only user name & profilePicture
}).populate({
path: 'reportTo', // attribute name of Model
model: "Post",
}).sort({ _id: -1 })
: req.query.type === "Profile" ? await Report.find({reportTo: req.params.id}).populate({
path: 'reportFrom', // attribute name of Model
model: "User",
select: "name profilePicture",
}).populate({
path: 'reportTo', // attribute name of Model
model: "User",
select: "name profilePicture",
})
.sort({ _id: -1 })
: null
return res.status(200).json(reports);
See the line 7 & 15, you can clearly see how I use two different refs for same attribute. In first case, reportTo is refered to Post Model & in second case reportTo is refered to User Model.

Clean up dead references with Mongoose populate()

If a user has an array called "tags":
var User = new Schema({
email: {
type: String,
unique: true,
required: true
},
tags: [{
type: mongoose.Schema.Types.ObjectId,
ref:'Tag',
required: true
}],
created: {
type: Date,
default: Date.now
}
});
and I do a populate('tags') on a query:
User.findById(req.params.id)
.populate("tags")
.exec(function(err, user) { ... });
If one of the tags in the list has actually been deleted, is there a way to remove this dead reference in "tags"?
Currently, the returned user object IS returning the desired result -- ie. only tags that actually exist are in the tags array... however, if I look at the underlying document in mongodb, it still contains the dead tag id in the array.
Ideally, I would like to clean these references up lazily. Does anyone know of a good strategy to do this?
I've tried to find some built-in way to do that but seems that mongoose doesn't provide such functionality.
So I did something like this
User.findById(userId)
.populate('tags')
.exec((err, user) => {
user.tags = user.tags.filter(tag => tag != null);
res.send(user); // Return result as soon as you can
user.save(); // Save user without dead refs to database
})
This way every time you fetch user you also delete dead refs from the document. Also, you can create isUpdated boolean variable to not call user.save if there was no deleted refs.
const lengthBeforeFilter = user.tags.length;
let isUpdated = user.tags.length;
user.tags = user.tags.filter(tag => tag != null);
isUpdated = lengthBeforeFilter > user.tags.length;
res.send(user);
if (isUpdated) {
user.save();
}
Assuming you delete these tags via mongoose, you can use the post middleware.
This will be executed after you've deleted a tag.
tagSchema.post('remove', function(doc) {
//find all users with referenced tag
//remove doc._id from array
});
its sample retainNullValues: true
Example:
User.findById(req.params.id)
.populate({
path: "tag",
options: {
retainNullValues: true
}
})

how to instert populated documents without: casting it in ObjectId then populate

I would like to avoid this (see comments below):
var UserSchema = Schema({
name: String
});
var UserGroupSchema = Schema({
users: [ { type: Schema.Types.ObjectId, ref: 'user' } ]
});
var user = new User({ name: 'John' });
var userGroup = new userGroup();
userGroup.users.push(user); // auto cast user in its objectId ! How to avoid that ?
UserGroup.populate(userGroup, { path: 'users', model: 'User' }); // Get back the user object (this step should be obsolete)
I do not want to declare this:
var UserGroupSchema = Schema({
users: [ User ]
});
Because i want to be able to find my users without looking into UserGroups.
I really hope there is something to avoid that because i don't see how to write clean code this way.
Because i want to be able to find my users without looking into UserGroups.
Specifically that means that you can only use references, which means saving new users separately before pushing a reference onto (and saving) a UserGroup instance.
I assume that a user can belong to more than one UserGroup, which means using subdocs (the users : [ User ] variant) can't really be used anyway.

Mongoose: Populate a populated field

I'm using MongoDB as a log keeper for my app to then sync mobile clients. I have this models set up in NodeJS:
var UserArticle = new Schema({
date: { type: Number, default: Math.round((new Date()).getTime() / 1000) }, //Timestamp!
user: [{type: Schema.ObjectId, ref: "User"}],
article: [{type: Schema.ObjectId, ref: "Article"}],
place: Number,
read: Number,
starred: Number,
source: String
});
mongoose.model("UserArticle",UserArticle);
var Log = new Schema({
user: [{type: Schema.ObjectId, ref: "User"}],
action: Number, // O => Insert, 1 => Update, 2 => Delete
uarticle: [{type: Schema.ObjectId, ref: "UserArticle"}],
timestamp: { type: Number, default: Math.round((new Date()).getTime() / 1000) }
});
mongoose.model("Log",Log);
When I want to retrive the log I use the follwing code:
var log = mongoose.model('Log');
log
.where("user", req.session.user)
.desc("timestamp")
.populate("uarticle")
.populate("uarticle.article")
.run(function (err, articles) {
if (err) {
console.log(err);
res.send(500);
return;
}
res.json(articles);
As you can see, I want mongoose to populate the "uarticle" field from the Log collection and, then, I want to populate the "article" field of the UserArticle ("uarticle").
But, using this code, Mongoose only populates "uarticle" using the UserArticle Model, but not the article field inside of uarticle.
Is it possible to accomplish it using Mongoose and populate() or I should do something else?
Thank you,
From what I've checked in the documentation and from what I hear from you, this cannot be achieved, but you can populate the "uarticle.article" documents yourself in the callback function.
However I want to point out another aspect which I consider more important. You have documents in collection A which reference collection B, and in collection B's documents you have another reference to documents in collection C.
You are either doing this wrong (I'm referring to the database structure), or you should be using a relational database such as MySQL here. MongoDB's power relies in the fact you can embed more information in documents, thus having to make lesser queries (having your data in a single collection). While referencing something is ok, having a reference and then another reference doesn't seem like you're taking the full advantage of MongoDB here.
Perhaps you would like to share your situation and the database structure so we could help you out more.
You can use the mongoose-deep-populate plugin to do this. Usage:
User.find({}, function (err, users) {
User.deepPopulate(users, 'uarticle.article', function (err, users) {
// now each user document includes uarticle and each uarticle includes article
})
})
Disclaimer: I'm the author of the plugin.
I faced the same problem,but after hours of efforts i find the solution.It can be without using any external plugin:)
applicantListToExport: function (query, callback) {
this
.find(query).select({'advtId': 0})
.populate({
path: 'influId',
model: 'influencer',
select: { '_id': 1,'user':1},
populate: {
path: 'userid',
model: 'User'
}
})
.populate('campaignId',{'campaignTitle':1})
.exec(callback);
}
Mongoose v5.5.5 seems to allow populate on a populated document.
You can even provide an array of multiple fields to populate on the populated document
var batch = await mstsBatchModel.findOne({_id: req.body.batchId})
.populate({path: 'loggedInUser', select: 'fname lname', model: 'userModel'})
.populate({path: 'invoiceIdArray', model: 'invoiceModel',
populate: [
{path: 'updatedBy', select: 'fname lname', model: 'userModel'},
{path: 'createdBy', select: 'fname lname', model: 'userModel'},
{path: 'aircraftId', select: 'tailNum', model: 'aircraftModel'}
]});
how about something like:
populate_deep = function(type, instance, complete, seen)
{
if (!seen)
seen = {};
if (seen[instance._id])
{
complete();
return;
}
seen[instance._id] = true;
// use meta util to get all "references" from the schema
var refs = meta.get_references(meta.schema(type));
if (!refs)
{
complete();
return;
}
var opts = [];
for (var i=0; i<refs.length; i++)
opts.push({path: refs[i].name, model: refs[i].ref});
mongoose.model(type).populate(instance, opts, function(err,o){
utils.forEach(refs, function (ref, next) {
if (ref.is_array)
utils.forEach(o[ref.name], function (v, lnext) {
populate_deep(ref.ref_type, v, lnext, seen);
}, next);
else
populate_deep(ref.ref_type, o[ref.name], next, seen);
}, complete);
});
}
meta utils is rough... want the src?
or you can simply pass an obj to the populate as:
const myFilterObj = {};
const populateObj = {
path: "parentFileds",
populate: {
path: "childFileds",
select: "childFiledsToSelect"
},
select: "parentFiledsToSelect"
};
Model.find(myFilterObj)
.populate(populateObj).exec((err, data) => console.log(data) );