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");
}
Related
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
I have this schema:
var UserSchema = mongoose.Schema({
analytic: {
type: Object,
default: {
today:[],
weekly:[],
monthly:[],
yearly:[],
allTime:[]
}
}
});
let User = mongoose.model("bloger", UserSchema);
module.exports = {User};
and I am trying to save some data into one of the arrays like so:
User.findOne({username:username}, (e, user) => {
if (e) {
res.send('error fetching post')
}
else if (!user) {
res.send('no user found')
}
else if (user) {
user.analytic.today.push(req.body.visitor) // push the data object to the array
user.save((e, doc) => {
if (e) {
res.send(e)
}
if (doc) {
console.log('user saved')
res.send(doc)
}
})
}
})
})
I am getting the doc object on save() and not the e so I though it should have save it but it wasn't.
I have had a similar issue before this is because I am not defining a new Model I am just passing a JSON object.
Instead of saving the object you need to create a new model and save that.
Try creating a new model passing the save into it like below;
var newUser = new User(user);
newUser.save((e, doc) {
if (e) {
res.send(e)
}
if (doc) {
console.log('user saved')
res.send(doc)
}
});
Making sure you require the User Model inside the script.
Performing deep modifications in objects not in your schema makes Mongoose oblivious to those changes, preventing it from knowing what to save (and from making efficient atomic updates). The end result is that, when calling .save, Mongoose thinks there's nothing modified and therefore does nothing.
In your particular scenario, you have two options:
1. Add your analytics sub-arrays to your schema
This is the best option and allows for finer control of everything:
const UserSchema mongoose.Schema({
analytic: {
today: [{}],
weekly: [{}],
monthly: [{}],
yearly: [{}],
allTime: [{}],
}
});
With this, those arrays are now known to Mongoose and everything should work correctly.
Note that you don't need defaults in this case, as Mongoose is smart enough to create the arrays as needed.
2. Manually mark modified object as modified
If for any reason you don't want or can't modify the schema, you can manually mark the analytic object as modifies so Mongoose knows of it:
user.analytic.today.push(req.body.visitor) // push the data object to the array
user.markModified('analytic'); // mark field as modified
user.save(...);
This signals Mongoose that analytic or any of its children have changed and triggers an update when calling .save. Note however that Mongoose views this as a full change in the object, while with option 1 it can use $push instead.
I am trying to mock out a user for testing out my application, and I have gotten to the point where I can create a test user and log them into the mirror instance of my app.
I need to compare the gmail addresses for peoples accounts, and to test this functionality, I want to add a test email address under user.services.google.email within the Meteor users account database (which is where the accounts-google package stores it, I don't need to mock out an entire user account yet).
What I can't figure out is how to append this information, instead of just overwriting what is already there, this is what my code looks like:
if (Meteor.users.find().count() === 0) {
var testUserDetails = {
email: 'testEmail#gmail.com',
password: 'testPassword'
};
console.log("Creating the Test User");
var newUserId = Accounts.createUser(testUserDetails);
Meteor.users.update({
_id: newUserId
}, {
$set: {
services: {
google: {
email: "testEmail#gmail.com"
}
}
}
});
} else {
console.log("There are already users in the Test database");
}
console.log('***** Finished loading default fixtures *****');
},
And this is what a user looks like:
{
"_id" : "Dw2xQPDwKp58RozC4",
"createdAt" : ISODate("2015-07-30T04:02:03.261Z"),
"services" : {
"password" : {
"bcrypt" : "asdfasdfasdfdsafsadfasdsdsawf"
},
"resume" : {
"loginTokens" : [ ]
}
},
"emails" : [
{
"address" : "testEmail#gmail.com",
"verified" : false
}
]
}
Now $set just rewrites everything within services, and there is no $push operation for mongo or for js, so how should I go about doing this? Should I consume the object and parse it manually?
*Note I have also tried using Meteor's Accounts.onCreateUser(function(options, user) but facing the same issue.
[...] there is no $push operation for mongo [...]
Sure, there is a $push operator, which appends a specified value to an array.
However, I think what you are trying to do is to update a document and keep all values which are already set.
Here is how you can do that:
Query the document first to get the object you want to set.
Update the respective object.
Run the MongoDB update operation to set the new object.
For instance:
var user = Meteor.users.findOne({
_id: newUserId
});
var servicesUserData = user.services;
servicesUserData.google.email = "your_new_email#gmail.com";
Meteor.users.update({
_id: newUserId
}, {
$set: {
"services": {
servicesUserData
}
}
});
A few questions about storing user data in MongoDB. What is the best place in mongo to store user specific data, such as User settings, User photo url, User friends, User events?
In Mongo, user data is stored in:
Meteor
/ Collections
/ users
/ _id
/ profile
/ services
Should I add there a new collections? In a following way:
/ events / _id's
/ friends / _id's
/ messages / _id's
/ settings
How should I publish user's private data and manipulate this collections, to be sure it's save and no one else will modify or have access to private data of another person.
You can add data to the users profile field like this:
Meteor.users.update( id, { $set: { 'profile.friends': someValue } } );
To only publish specific fields you can do something like this:
Meteor.publish( 'users', function () {
return Meteor.users.find( {}, { fields: { 'profile.friends': 1 } } );
});
Hope this helps.
Normalization
"Database normalization is the process of organizing the attributes and tables of a relational database to minimize data redundancy."
MongoDB is a non relational database. This makes normalized data hard to query for. This is why in MongoDB we denormalize data. That makes querying for it easier.
It depends on your use-case. The question is basically when to demormalize. It's mostly a matter of opinion. But objective here are some pros and cons:
Pros to demormalization
It's easier to retrieve data (Due to Mongo not beeing a relational DB)
It performs better if you are always getting the data in bulk
Cons to demormalization
It doesn't scale well for things like user.messages (You can't just publicize some messages)
In your case I'd definitly go for seperate collections for events, friends and messages. Setting can't expand infinitly. So I'd put it into the users collection.
Security
I'd use a publications and allow and deny for this. Let me make an example for Messages:
Collection
Messages = new Mongo.Collection('Messages')
Messages.insert({
sender: Meteor.userId,
recipient: Meteor.users.findOne()._id,
message: 'Hello world!'
})
Publication
Meteor.publish('userMessages', function (limit) {
return Messages.subscribe({
$or: [
{sender: this.userId},
{recipient: this.userId}
]
}, {limit: limit})
})
Allow
function ownsMessage (user, msg) {
return msg.sender === user ? true : false
}
Messages.allow({
insert: function (userId, newDoc) {
!!userId
},
update: function (userId, oldDoc, newDoc) {
if(
ownsMessage(userId, oldDoc) &&
ownsMessage(userId, newDoc)
) return true
return false
},
remove: function () {
return false
}
})
This code is untested, so it might contain small errors
In my social app(like as FB) I have a strange need to merge two cursors of the same collection users in one publish!
Meteor server print this error:
"Publish function returned multiple cursors for collection users".
Maybe this can not be done in Meteor 0.7.2, perhaps I'm mistaken approach.
But I have seen the structure of a cursor is pretty simple as I could make a simple array merge and return back a Cursor?
CLIENT
Meteor.subscribe('friendById', friend._id, function() {
//here show my friend data and his friends
});
SERVER
//shared functions in lib(NOT EDITABLE)
getUsersByIds = function(usersIds) {
return Meteor.users.find({_id: {$in: usersIds} },
{
fields: { // limited fields(FRIEND OF FRIEND)
username: 1,
avatar_url: 1
}
});
};
getFriendById = function(userId) {
return Meteor.users.find(userId,
{
fields: { // full fields(ONLY FOR FRIENDS)
username: 1,
avatar_url: 1,
online: 1,
favorites: 1,
follow: 1,
friends: 1
}
});
};
Meteor.publish('friendById', function(userId) { //publish user data and his friends
if(this.userId && userId)
{
var userCur = getFriendById(userId),
userFriends = userCur.fetch()[0].friends,
retCurs = [];
//every return friend data
retCurs.push( userCur );
//if user has friends! returns them but with limited fields:
if(userFriends.length > 0)
retCurs.push( getUsersByIds(userFriends) );
//FIXME ERROR "Publish function returned multiple cursors for collection users"
return retCurs; //return one or more cursor
}
else
this.ready();
});
See bold red text in the documentation:
If you return multiple cursors in an array, they currently must all be from different collections.
There is the smart-publish package which adds this ability to use on publish to manage multiple cursors on the same collection. It is relatively new.
Either that or manually manage the cursors by using 'this.added', 'this.removed', and 'this.changed' inside the publish.
SOLUTION
Meteor.publish('friendById', function(userId) {
if(this.userId && userId)
{
var userCur = getFriendById(userId), //user full fields
userData = userCur.fetch()[0],
isFriend = userData.friends.indexOf(this.userId) != -1,
retCurs = [];
//user and his friends with limited fields
retCurs.push( getUsersByIds( _.union(userId, userData.friends) ));
if(isFriend)
{
console.log('IS FRIEND');
this.added('users',userId, userData); //MERGE full fields if friend
//..add more fields and collections in reCurs..
}
return retCurs;
}
else
this.ready();
});