Meteor and MongoDB: How to set an object in a table - meteor

I have a problem with Meteor and MongoDB. I'm coding a little game. I use accounts-ui and when an account is created I add some fields to this account :
Accounts.onCreateUser(function(options, user) {
user.money = 0;
user.rate = 0;
user.employees = [];
return user;
})
As you can see, I have a table named "employees".
When the user will click on a button to hire an employee, I want to update this table. Imagine the user hire a programmer, I want to update my table like this :
Accounts.onCreateUser(function(options, user) {
user.money = 0;
user.rate = 0;
user.employees = [{"programmer": 1}];
return user;
})
If the user hire an other programmer I want to increment the number of programmer. And if the user hire a designer I want to add it to the table like this :
Accounts.onCreateUser(function(options, user) {
user.money = 0;
user.rate = 0;
user.employees = [{"programmer": 2}, {"designer": 1}];
return user;
})
I'm a beginner in Meteor and MongoDB, I don't understand how I can do this.

If your employees is a key-value pair as you described, I'd use an object instead of an array.
user.employees = {};
It makes MongoDB operations easier:
Meteor.users.update({_id: someId}, {$inc: {"employees.programmer": 1}});
This server-side code adds one to the user's programmers. If the user doesn't have a programmer yet, this code will create the first one.

You can simply write query like this:
If( programmer hired ) {
Meteor.users.update({_id: someId}, {$inc: {"employees.programmer": 1}});
} else if ( designer hired ) {
Meteor.users.update({_id: someId}, {$inc: {"employees.designer": 1}});
} else if ( programmer and designer hired ) {
Meteor.users.update({_id: someId}, {$inc: {"employees.programmer": 1, "employees.designer": 1}});
}

You can add an employeesattribute to the user like this :
Accounts.createUser( {
username: 'test',
email: 'email#email.com',
password: 'test',
employees: {}
});
And update the employees :
Add an employee
Meteor.users.update(idUser, {$addToSet: {employees: {'a': 1}}});
Meteor.users.update(idUser, {$addToSet: {employees: {'b': 1}}});
Increment the number of employee a :
Meteor.users.update(idUser, {$pull: {employees: {'a': 1}}});
Meteor.users.update(idUser, {$addToSet: {employees: {'a': 2}}});

Related

Firebase denormalization data consistency issue

I'm currently using Ionic CLI 3.19 with Cordova CLI 7.1.0 (#ionic-app-script 3.1.4)
The problem that I’m currently facing with is, I should update friends node values simultaneously every time the related data get changed from elsewhere. I’d like to clarify my objective with some screenshots to make it more clear.
As you can see from the image below, each child node consists of a user array that has a user id as a key of friends node. The reason why I store as an array is because each user could have many friends.
In this example, Jeff Kim has one friend which is John Doe vice versa.
When data in users node gets changed for some reason, I want the related data in friends node also want them to be updated too.
For example, when Jeff Kim changed his profile photo or statusMessage all the same uid that reside in friends node which matches with Jeff Kim’s uid need to be updated based on what user has changed.
user-service.ts
constructor(private afAuth: AngularFireAuth, private afDB: AngularFireDatabase,){
this.afAuth.authState.do(user => {
this.authState = user;
if (user) {
this.updateOnConnect();
this.updateOnDisconnect();
}
}).subscribe();
}
sendFriendRequest(recipient: string, sender: User) {
let senderInfo = {
uid: sender.uid,
displayName: sender.displayName,
photoURL: sender.photoURL,
statusMessage: sender.statusMessage,
currentActiveStatus: sender.currentActiveStatus,
username: sender.username,
email: sender.email,
timestamp: Date.now(),
message: 'wants to be friend with you.'
}
return new Promise((resolve, reject) => {
this.afDB.list(`friend-requests/${recipient}`).push(senderInfo).then(() => {
resolve({'status': true, 'message': 'Friend request has sent.'});
}, error => reject({'status': false, 'message': error}));
});
}
fetchFriendRequest() {
return this.afDB.list(`friend-requests/${this.currentUserId}`).valueChanges();
}
acceptFriendRequest(sender: User, user: User) {
let acceptedUserInfo = {
uid: sender.uid,
displayName: sender.displayName,
photoURL: sender.photoURL,
statusMessage: sender.statusMessage,
currentActiveStatus: sender.currentActiveStatus,
username: sender.username,
email: sender.email
}
this.afDB.list(`friends/${sender.uid}`).push(user);
this.afDB.list(`friends/${this.currentUserId}`).push(acceptedUserI
this.removeCompletedFriendRequest(sender.uid);
}
According to this clip that I've just watched, it looks like I did something called Denormalization and the solution might be using Multi-path updates to change data with consistency. Data consistency with Multi-path updates. However, it's kinda tricky to fully understand and start writing some code.
I've done some sort of practice to make sure update data in multiple locations without calling .update method twice.
// I have changed updateUsername method from the code A to code B
// Code A
updateUsername(username: string) {
let data = {};
data[username] = this.currentUserId;
this.afDB.object(`users/${this.currentUserId}`).update({'username': username});
this.afDB.object(`usernames`).update(data);
}
// Code B
updateUsername(username: string) {
const ref = firebase.database().ref();
let updateUsername = {};
updateUsername[`usernames/${username}`] = this.currentUserId;
updateUsername[`users/${this.currentUserId}/username`] = username;
ref.update(updateUsername);
}
I'm not trying to say this is a perfect code. But I've tried to figure this out on my own and here's what I've done so far.
Assume that I'm currently signed in as Jeff.
When I run this code all the associated data with Jeff in friends node gets changed, as well as Jeff's data in users node gets updated simultaneously.
The code needs to be improved by other firebase experts and also should be tested on a real test code.
According to the following thread, once('value' (which is, in general, a bad idea for optimal performance with Firebase). I should find out why this is bad.
friend.ts
getFriendList() {
const subscription = this.userService.getMyFriendList().subscribe((users: any) => {
users.map(u => {
this.userService.testMultiPathStatusMessageUpdate({uid: u.uid, statusMessage: 'Learning Firebase:)'});
});
this.friends = users;
console.log("FRIEND LIST#", users);
});
this.subscription.add(subscription);
}
user-service.ts
testMultiPathStatusMessageUpdate({uid, statusMessage}) {
if (uid === null || uid === undefined)
return;
const rootRef = firebase.database().ref();
const query = rootRef.child(`friends/${uid}`).orderByChild('uid').equalTo(this.currentUserId);
return query.once('value').then(snapshot => {
let key = Object.keys(snapshot.val());
let updates = {};
console.log("key:", key);
key.forEach(key => {
console.log("checking..", key);
updates[`friends/${uid}/${key}/statusMessage`] = statusMessage;
});
updates[`users/${this.currentUserId}/statusMessage`] = statusMessage;
return rootRef.update(updates);
});
}
The code below works fine when updating status to online but not offline.
I don't think it's the correct approach.
updateOnConnect() {
return this.afDB.object('.info/connected').valueChanges()
.do(connected => {
let status = connected ? 'online' : 'offline'
this.updateCurrentUserActiveStatusTo(status)
this.testMultiPathStatusUpdate(status)
})
.subscribe()
}
updateOnDisconnect() {
firebase.database().ref().child(`users/${this.currentUserId}`)
.onDisconnect()
.update({currentActiveStatus: 'offline'});
this.testMultiPathStatusUpdate('offline');
}
private statusUpdate(uid, status) {
if (uid === null || uid === undefined)
return;
let rootRef = firebase.database().ref();
let query = rootRef.child(`friends/${uid}`).orderByChild('uid').equalTo(this.currentUserId);
return query.once('value').then(snapshot => {
let key = Object.keys(snapshot.val());
let updates = {};
key.forEach(key => {
console.log("checking..", key);
console.log("STATUS:", status);
updates[`friends/${uid}/${key}/currentActiveStatus`] = status;
});
return rootRef.update(updates);
});
}
testMultiPathStatusUpdate(status: string) {
this.afDB.list(`friends/${this.currentUserId}`).valueChanges()
.subscribe((users: any) => {
users.map(u => {
console.log("service U", u.uid);
this.statusUpdate(u.uid, status);
})
})
}
It does show offline in the console, but the changes do not appear in Firebase database.
Is there anyone who could help me? :(
I think you are right doing this denormalization, and your multi-path updates is in the right direction. But assuming several users can have several friends, I miss a loop in friends' table.
You should have tables users, friends and a userFriend. The last table is like a shortcut to find user inside friends, whitout it you need to iterate every friend to find which the user that needs to be updated.
I did a different approach in my first_app_example [angular 4 + firebase]. I removed the process from client and added it into server via onUpdate() in Cloud functions.
In the code bellow when user changes his name cloud function executes and update name in every review that the user already wrote. In my case client-side does not know about denormalization.
//Executed when user.name changes
exports.changeUserNameEvent = functions.database.ref('/users/{userID}/name').onUpdate(event =>{
let eventSnapshot = event.data;
let userID = event.params.userID;
let newValue = eventSnapshot.val();
let previousValue = eventSnapshot.previous.exists() ? eventSnapshot.previous.val() : '';
console.log(`[changeUserNameEvent] ${userID} |from: ${previousValue} to: ${newValue}`);
let userReviews = eventSnapshot.ref.root.child(`/users/${userID}/reviews/`);
let updateTask = userReviews.once('value', snap => {
let reviewIDs = Object.keys(snap.val());
let updates = {};
reviewIDs.forEach(key => { // <---- note that I loop in review. You should loop in your userFriend table
updates[`/reviews/${key}/ownerName`] = newValue;
});
return eventSnapshot.ref.root.update(updates);
});
return updateTask;
});
EDIT
Q: I structured friends node correctly or not
I prefer to replicate (denormalize) only the information that I need more often. Following this idea, you should just replicate 'userName' and 'photoURL' for example. You can aways access all friends' information in two steps:
let friends: string[];
for each friend in usrService.getFriend(userID)
friends.push(usrService.getUser(friend))
Q: you mean I should create a Lookup table?
The clip mentioned in your question, David East gave us an example how to denormalize. Originaly he has users and events. And in denormalization he creates eventAttendees that is like a vlookup (like you sad).
Q: Could you please give me an example?
Sure. I removed some user's information and add an extra field friendshipTypes
users
xxsxaxacdadID1
currentActiveStatus: online
email: zinzzkak#gmail.com
gender: Male
displayName: Jeff Kim
photoURL: https://firebase....
...
trteretteteeID2
currentActiveStatus: online
email: hahehahaheha#gmail.com
gender: Male
displayName: Joeh Doe
photoURL: https://firebase....
...
friends
xxsxaxacdadID1
trteretteteeID2
friendshipTypes: bestFriend //<--- extra information
displayName: Jeff Kim
photoURL: https://firebase....
trteretteteeID2
xxsxaxacdadID1
friendshipTypes: justAfriend //<--- extra information
displayName: John Doe
photoURL: https://firebase....
userfriends
xxsxaxacdadID1
trteretteteeID2: true
hgjkhgkhgjhgID3: true
trteretteteeID2
trteretteteeID2: true

Meteor.users.find() - how to filter by group in alanning:roles package

I'm trying to make an admin section on my website. The admin can go to the admin page and see a table of the users in his group. I only want to publish a the users that are in that admin's group. E.g. he is in the group ['soccer'] but I wouldn't want him to see all users in the ['hockey'] group. A user can be in more than one group but I should be able to figure that out once I understand how to query it.
Anyways, here's what I have so far:
Meteor.publish('users', function() {
groups = Roles.getGroupsForUser(this.userId)
group = groups[0] //Just get the first group for now
// If the user is an admin of any group (the first one in the array)
if (Roles.userIsInRole(this.userId, ['admin'], group)) {
return Meteor.users.find({roles: {$in: groups}},
{fields: {emails: 1,
createdAt: 1,
roles: 1
}
});
} else {
console.log('null')
return null;
}
});
My subscription in my router:
Meteor.subscribe("users")
Now when I replace:
{roles: {$in: groups}}
with just:
{}
It works, but my table contains all users instead of just the users of a group.
What query should I put to make this work? I'm using the roles package by alanning.
you can do a simple query and add your fields, sorting, what you want ;-)
Meteor.users.find({'roles.__global_roles__':'admin'}).fetch()
__global_roles is the default group. replace it with the group you need.
http://docs.mongodb.org/manual/tutorial/query-documents/#match-an-array-element
This code has about a 5% chance of working out of the box because I have no collection to test this on, I have no way of running this code, I don't have the roles package, I don't have your users database, and I've never done .map on a cursor before, haha.
/server/methods.js
Meteor.methods({
returnAdminUsers: function(){
var results = [];
var results = Roles.getUsersInRole(['admin']).map(function(user, index, originalCursor){
var result = {
_id: user._id,
emails: user.emails,
createdAt: user.createdAt,
roles: user.roles
};
console.log("result: ", result);
return result;
});
console.log("all results - this needs to get returned: ", results);
return results;
}
})
/client/somethingsomething.js
Meteor.call("returnAdminUsers", function(error, result){
if(error){
console.log("error from returnAdminUsers: ", error);
} else {
Session.set("adminUsers", result);
}
});
And then in your helpers:
Template.somethingsomething.helpers({
adminUsers: function(){ return Session.get("adminUsers") }
});
/client/somethingsomething.html
{{#each adminUsers}}
The ID is {{_id}}
{{/each}}
If you are using meteor-tabular/TabularTables this answer can help:
Display only users with a specific role in meteor-tabular table
If you want to check all the rules of such a group:
Meteor.users.find({
'roles.myrole': {$exists : true}
});
Or with just a few:
Meteor.users.find({
'roles.myrole': {$in: ['viewer', 'editor']}
});

Most efficient way to ensure user owns document on update?

I'm using Meteor methods to update documents so I can share them easier and have more control. However i've ran into a problem with checking ownership.
How should I check to make sure the user calling the update method is the owner of the document? Currently i'm grabbing the document first then running the update.
Is there a better pattern to accomplish this?
Meteor.methods({
'Listing.update': function(docId, data) {
var doc = db.listings.findOne({_id: docId}) || {};
if (doc.userId !== this.userId) {
throw new Meteor.Error(504, "You don't own post");
}
// ensure data is the type we expect
check(data, {
title: String,
desc: String
});
return db.listings.update(docId, {$set: data});
}
});
You don't need the additional db call to fetch the original doc, just make the userId an additional criteria in the update selector. If no doc exists with the correct _id and userId no update will be done. update returns the number of docs updated so it will return 1 on success and 0 on failure.
like this:
'Listing.update': function(docId, data) {
var self = this;
check(data, {
title: String,
desc: String
});
if ( ! self.userId )
throw new Meteor.Error(500, 'Must be logged in to update listing');
res = db.listings.update({_id: docId, userId: self.userId}, {$set: data});
if ( res === 0 )
throw new Meteor.Error( 504, "You do not own a post with that id" );
return res;
}
Also, if you use findOne to check a document's existence, use the fields option to limit what you return from the db. Usually just {fields: {_id:1}}.

Adding new users to a collection and having them render on a list automatically?

So im doing the leaderboard example on the meteor site but instead of the predefined data I start off with, I want to create a new name and score that automatically appears on the screen when someone creates an account, so at this point I get the name and the score on the screen only after I create an account and hit the refresh button on the browser, what do I want to do so that I don't have to hit the refresh button and the user login name and score automatically appears on the screen?
do I want to use deps.flush() or meteor.render somehow?
server.js
// newUser Method
Meteor.methods({
newUser: function() {
var user = Meteor.user();
userVar = {
name: user.username,
score: 0
};
Players.insert(userVar);
}
});
client.js
Deps.autorun(function() {
Meteor.call('newUser');
});
Template.leaderboard.players = function () {
return Players.find({}, {sort: {score: -1, name: 1}});
};
Template.leaderboard.selected_name = function () {
var player = Players.findOne(Session.get("selected_player"));
return player && player.name;
};
Template.player.selected = function () {
return Session.equals("selected_player", this._id) ? "selected" : '';
};
Template.leaderboard.events({
'click input.inc': function () {
Players.update(Session.get("selected_player"), {$inc: {score: 5}});
}
});
Template.player.events({
'click': function () {
Session.set("selected_player", this._id);
}
});
If your starting point is a working version of the example then you should be seeing reactive changes to the web page each time the Players collection changes. Deps.flush or Meteor.render are unnecessary.
The Deps.autorun() function you have is only called once when the client starts. At that point you may not have a user and your method will fail when you try to get a username from the null variable, 'user'.
To trigger the autorun each login and when you have a user you need it to refer to a reactive data source. If you rewrite it like this you should see a new player showing up each time a user logs in:
//on client
Deps.autorun( function(){
if ( Meteor.userId() ){
Meteor.call('newUser');
}
});
I also wonder if your method on the server will have a problem as this.userId is how I usually get the user information inside a method. Here is an alternative to avoid the method and just insert the player on the client:
//on client
Deps.autorun( function(){
var user = Meteor.user();
if ( user ) { //insert will run on login or any change in the user
var userVar = {
name: user.username,
score: 0
};
Players.insert(userVar);
}
});
So I assume then that "player" records belong to a user in some way? So when you create a new user, you create their new default player record?
Maybe you just need your helpers to check that a player record for the user exists, and if not, create it.
Template.leaderboard.players = function () {
var players = Players.find({/* Get players for this user */ }, {sort: {score: -1, name: 1}});
if(!players) {
players = /* Insert default player record */
}
return players;
};

async nodejs querying and processing results

I have an array of objects taken from mongodb. Every element in the array is a post, with author as user_id. Now i wish to find the user info related to the user_id.
Since node uses async methods to find the data from db, the forEach loop finishes before the callbacks finish.
docs.forEach(function(doc, index){
//get the user for this doc
User.find({_id: mongo.BSONPure.ObjectID(doc.user_id)}, {name: 1, username: 1, email: 1}).skip(0).limit(1).toArray(function(err, user){
user = user[0]
if(err) {
throw new Error(err)
} else {
docs[index].user = user
if(doc.for_id) {
User.find({_id: mongo.BSONPure.ObjectID(doc.for_id)}, {name: 1, username: 1, email: 1}).skip(0).limit(1).toArray(function(err, for_user){
for_user = for_user[0]
if(err) {
throw new Error(err)
} else {
docs[index].for_user = for_user
}
})
}
}
})
})
So at the end of this loop, if i send a cb(docs), docs do not have the user and for_user attribute. How do I overcome this?
Use Step for node.js. It will run your functions in serial order
var Step = require('step');
Step( docs.forEach(...), function() { cb(docs); } );
Or if you know the total number of records, you can call the callback when you're done processing the last one. Something like this
var count = docs.count(); // or something
var processed = 0;
docs.forEach(... if (++processed == count) cb(docs); );

Resources