MongoDB update many using the values of the user - javascript

I've written a function to execute hourly which looks up a user and finds some values and then pushes those values into a history collection that records the hourly updated values. I've written this so far as a test just finding a user by their ID but now I need to roll this out to my entire database of 50,000+ users.
From what I've read using updateMany is a lot more performant but I'm not entirely sure how to retrieve the document detail of the record that is being updated at the time.
Here is my code so far, which you can see I'm first looking up the user and then grabbing their valuation details which I'd like to then push into a history collection.
exports.updateUserValuationHistoric = () => {
User.find({ _id: "609961fdd989613914ef7216" })
.populate('UserValuationHistory')
.exec((err, userDoc) => {
if (err){
console.log('[ERROR]: ', err)
}
const updatedValuationHistory = {
totalValuation: userDoc[0].valuation.totalValuation,
comicsValuation: userDoc[0].valuation.comicsValuation,
collectiblesValuation: userDoc[0].valuation.collectiblesValuation,
omiValuation: userDoc[0].valuation.omiValuation
}
UserValuationHistory.findOneAndUpdate(
{ user: userDoc[0]._id },
{ $push: {
'history': updatedValuationHistory
}},
{upsert: true, new: true}
)
.exec((error, updated) => {
if (error){
console.log('[ERROR]: Unable to update the user valuation history.')
} else {
console.log('[SUCCESS]: User historic valuation has been updated.')
}
})
})
}
Any help is greatly appreciated!
User model:
https://pastebin.com/7MWBVHf3
Historic model:
https://pastebin.com/nkTGztJY

Related

Meteor MongoDB Setting field of array per document

I have a Documents in a Collection that have a field that is an Array (foo). This is an Array of other subdocuments. I want to set the same field (bar) for each subdocument in each document to the same value. This value comes from a checkbox.
So..my client-side code is something like
'click #checkAll'(e, template) {
const target = e.target;
const checked = $(target).prop('checked');
//Call Server Method to update list of Docs
const docIds = getIds();
Meteor.call('updateAllSubDocs', docIds, checked);
}
I tried using https://docs.mongodb.com/manual/reference/operator/update/positional-all/#positional-update-all
And came up with the following for my Server helper method.
'updateAllSubDocs'(ids, checked) {
Items.update({ _id: { $in: ids } }, { $set: { "foo.$[].bar": bar } },
{ multi: true }, function (err, result) {
if (err) {
throw new Meteor.Error('error updating');
}
});
}
But that throws an error 'foo.$[].bar is not allowed by the Schema'. Any ideas?
I'm using SimpleSchema for both the parent and subdocument
Thanks!
Try passing an option to bypass Simple Schema. It might be lacking support for this (somewhat) newer Mongo feature.
bypassCollection2
Example:
Items.update({ _id: { $in: ids } }, { $set: { "foo.$[].bar": bar } },
{ multi: true, bypassCollection2: true }, function (err, result) {
if (err) {
throw new Meteor.Error('error updating');
}
});
Old answer:
Since you say you need to make a unique update for each document it sounds like bulk updating is the way to go in this case. Here's an example of how to do this in Meteor.
if (docsToUpdate.length < 1) return
const bulk = MyCollection.rawCollection().initializeUnorderedBulkOp()
for (const myDoc of docsToUpdate) {
bulk.find({ _id: myDoc._id }).updateOne({ $set: update })
}
Promise.await(bulk.execute()) // or use regular await if you want...
Note we exit the function early if there's no docs because bulk.execute() throws an exception if there's no operations to process.
If your data have different data in the $set for each entry on array, I think you need a loop in server side.
Mongo has Bulk operations, but I don't know if you can call them using Collection.rawCollection().XXXXX
I've used rawCollection() to access aggregate and it works fine to me. Maybe work with bulk operations.

Pagination with populated data in mongo

I'm struggling with a problem of fetching data from DB and applying "pagination" functionality. My case is that I have webstore with multiple users. Every user can publish their profile and create their products. On the website, I'd like to show the products of users who did publish their profiles.
Currently, this is my code:
Product.find({ active: true, deleted: false })
.sort({ created: -1 })
.populate({
path: 'user',
match: { isPublished: true },
select: 'details slug name',
})
.exec((error, products) => {
if (error) {
return response.status(500).send({ error: 'There was an error.' });
}
if (products === null) {
return response.status(404).send({ error: 'Products not found.' });
}
const productsWithPublishedTrainerProfile = _.filter(products, product => !_.isNull(product.user));
return response.status(200).send(_.map(productsWithPublishedTrainerProfile, product => mapProduct(product)));
});
It works - It fetches all the active products, populate the user who created the product and if their profile is published then the product is not filtered out.
Now I need to implement "pagination" functionality which means that I want to use .skip() and .limit() functions but this skip/limit will be run on all the products (not only the filtered ones.) what means that the endpoint won't be returning the exact number of products (set in the limit() function.
I wonder if there is any solution for that. I read about .aggregate with setting $lookup, $skip and $limit but after hours of fighting with it I couldn't make it work.
I will really appreciate any help ✌️

Mongoose - Find by array of ids, but retrieve limited results (1 for each id)

i have my model called "Conversations", and my model "Messages", right now i want to retrieve all conversations with the last Message attached (only 1 message per conversation), so i filtered the conversationids and i queried the messages, but i'm not able to get this messages (last messages) for each conversation, thanks in advance.
let conversations = await ConversationModel.find({});
const conversationIds = conversations.map(conversation => conversation._id)
// ConversationIds is basically ["conversation1", "conversation2", "conversation3"]
// Te problem is here, i want to attach the las message for each conversation, if i put limit(1)
// i will get 1 record for all query, but i want the last message record for each conversation.
MessageModel.find({ _id: { "$in" : conversationIds} }, ...);
From information gathered in comments; This is possible to achieve in a case where MessageModel documents contain a time-stamp to identify latest of them.
Idea: Filter messages based on conversationIds via $match, sort them by timestamp for the next stage where $group on conversation reference (lets say conversation_id) and pick latest of them by $first accumulator.
Aggregation Query: playground link
db.collection.aggregate([
{
$match: {
conversation_id: {
$in: conversationIds
}
}
},
{
$sort: {
timestamp: -1
}
},
{
$group: {
_id: "$conversation_id",
latest_doc: {
$first: "$$ROOT"
}
}
}
]);

How to add additional fields to user documents (not in profile)?

I know that the classic way to add data to user collection is in profile array, but according to this document, it is not the best way to store data.
Is there an alternative to that, for example to create a field in the root of user collection at the same level with default fields (_id, username, etc.)?
There is nothing wrong per-se with the profile field, other than the fact that a users can (currently) directly update their own profile by default.
I don't find this behavior desired, as a user could store arbitrary data in the profile.
This may become a real security risk if the developer uses that field as a source of authority; for example, stores the user's groups or roles in it.
In this case, users could set their own permissions and roles.
This is caused by this code:
users.allow({
// clients can modify the profile field of their own document, and
// nothing else.
update: function (userId, user, fields, modifier) {
// make sure it is our record
if (user._id !== userId)
return false;
// user can only modify the 'profile' field. sets to multiple
// sub-keys (eg profile.foo and profile.bar) are merged into entry
// in the fields list.
if (fields.length !== 1 || fields[0] !== 'profile')
return false;
return true;
}
});
The first thing to do is to restrict writes to it:
Meteor.users.deny({
update() {
return true;
}
});
It could then be updated using methods and other authorized code.
If you add your own fields and want to publish them to the currently logged-in user, you can do so by using an automatic publication:
Meteor.publish(null, function () {
if (this.userId) {
return Meteor.users.find({
_id: this.userId
}, {
fields: {
yourCustomField1: 1,
yourCustomField2: 1
}
});
} else {
return this.ready();
}
});
Meteor.users is just a normal Mongo.Collection, so modifying it is done just like any other Collection. There is also the creation hook, Accounts.onCreateUser which allows you to add custom data to the user object when it is first created, as mentioned in #MatthiasEckhart's answer.
You could add extra fields to user documents via the accountsServer.onCreateUser(func) function.
For example:
if (Meteor.isServer) {
Accounts.onCreateUser(function(options, user) {
_.extend(user, {
myValue: "value",
myArray: [],
myObject: {
key: "value"
}
});
});
}
Please note: By default, the following Meteor.users fields are published to the client username, emails and profile. As a consequence, you need to publish any additional fields.
For instance:
if (Meteor.isServer) {
Meteor.publish("user", function() {
if (this.userId) return Meteor.users.find({
_id: this.userId
}, {
fields: {
'myValue': 1,
'myArray': 1,
'myObject': 1
}
});
else this.ready();
});
}
if (Meteor.isClient) {
Meteor.subscribe("user");
}

How do I link each user to their data in Firebase?

I plan to create a main tree named users which will include the name different users used as username. So, from each username will be included their data e.g. Full Name, Address, Phone No.
I want to know how to get each user's data when they log in on their profile.
First of all i suggest you spend some time getting familiar with firebase by reading the Firebase Guide (Link to old Firebase Guide). Everything you need to know to answer your own question is available there. But for simplicity i will put an example here:
Lets start with security, here are the basic firebase rules you need for this example: (source: Understanding Security) (old source: Understanding Security)
{
"rules": {
"users": {
"$user_id": {
".write": "$user_id === auth.uid"
}
}
}
}
I will skip the actual user creation and logging in and focus on the question about storing and retrieving user data.
Storing data: (source: Firebase Authentication) (old source: User Authentication)
// Get a reference to the database service
var database = firebase.database();
// save the user's profile into Firebase so we can list users,
// use them in Security and Firebase Rules, and show profiles
function writeUserData(userId, name, email, imageUrl) {
firebase.database().ref('users/' + userId).set({
username: name,
email: email
//some more user data
});
}
The resulting firebase data will look like this:
{
"users": {
"simplelogin:213": {
"username": "password",
"email": "bobtony"
},
"twitter:123": {
"username": "twitter",
"email": "Andrew Lee"
},
"facebook:456": {
"username": "facebook",
"email": "James Tamplin"
}
}
}
And last but not least the retreiving of the data, this can be done in several ways but for this example i'm gonna use a simple example from the firebase guide: (source: Read and Write data) (old source: Retreiving Data)
//Get the current userID
var userId = firebase.auth().currentUser.uid;
//Get the user data
return firebase.database().ref('/users/' + userId).once('value').then(function(snapshot) {
//Do something with your user data located in snapshot
});
EDIT: Added example of return data
So when you are logged in as user twitter:123 you will get a reference to the location based on your user id and will get this data:
"twitter:123": {
"username": "twitter",
"email": "Andrew Lee"
}
Though I agree with Andre about setting the rules for good security - I would handle the data a bit differently. Instead of generating the string I use the child() method. It's a matter of personal preference.
Get the UID and define a data object:
let user = firebase.auth().currentUser
let uid = user.uid
let yourdata = { foo: 'something', bar: 'other'}
Save the data:
firebase.database().ref('users').child(uid).set(yourdata)
.then((data) => {
console.log('Saved Data', data)
})
.catch((error) => {
console.log('Storing Error', error)
})
Fetch the data:
firebase.database().ref('users').child(uid).once('value')
.then((data) => {
let fetchedData = data.val()
console.log('Fetched Data', fetchedData)
})
.catch((error) => {
console.log('Fetching Error', error)
})
Please notice that set() will override your data so you might want to use push() later on.

Categories