Meteor: How to subscribe/show usernames when this.userId is published - mongodb

Note: Whole sourcecode can be found here:
https://github.com/Julian-Th/crowducate-platform/
I have the following pub functions:
Meteor.publish('editableCourses', function () {
return Courses.find({"canEditCourse": { $in: [ this.userId ] } });
});
Meteor.publish("userData", function () {
if (this.userId) {
return Meteor.users.find({_id: this.userId},
{fields: {'realname': 1, 'username': 1, 'gender': 1, 'language': 1, 'biography': 1 }})
}
});
Meteor.publish("allUsernamesExceptCurrent", function () {
// Get current user
var currentUserId = this.userId;
// Get all users except current,
// only get username field
var users = Meteor.users.find(
{_id: {$ne: currentUserId}},
{fields: {username: 1}}
);
return users;
});
Here's the template:
<template name="myTeachingCourses">
<button class="btn btn-primary js-create-course"><i class="fa fa-plus"></i> Create a Course</button>
<hr>
<div class="row">
<ul class="thumbnails list-unstyled">
{{#each courses}}
{{>courseCard}}
{{/each}}
</ul>
</div>
</template>
The helper with subscription:
Template.myTeachingCourses.helpers({
'courses': function(){
return Courses.find();
}
});
Template.myTeachingCourses.created = function () {
// Get reference to template instance
var instance = this;
// Subscribe to all published courses
instance.subscribe("editableCourses");
};
The problem: The template doesn't render the created courses. A course creator can't see his own courses or add other collaborators. I wonder if the reason might be, because canEditCourse is now an array with usernames and not with IDs.
My JSON for Courses:
{
"_id" : "hRX8YABpubfZ4mps8",
"title" : "Course Title here",
"coverImageId" : "Qojwbi2hcP2KqsHEA",
"author" : "Name",
"keywords" : [
"test",
"meteor"
],
"published" : "true",
"about" : "Testing",
"canEditCourse" : [
"User1"
],
"createdById" : "zBn6vtufK2DgrHkxG",
"dateCreated" : ISODate("2015-12-29T21:42:46.936Z")
}
Via this question I found out that storing usernames in the array is not a good idea for now obvious reasons. In the beginning, I actually stored IDs instead of usernames but then changed it because to use the username for autocomplete with typeahead and rendering the IDs of all collaborators is not good thing to do. What is the best way to fix it? In other words, how is it possible to render all created courses.

So, as you answered yourself, your Courses collection field canEditCourse has an array of usernames, not userId, so the query will not return anything.

Related

MongoDB - Add / update to list of objects on document

I have a user collection, and each user has a list of products. I need to update a product using it's ID or add the product if it doesn't exist.
How can I update the product by it's id?
User Collection:
{
"_id": {
"$oid": "5fc06554266266edf5643231"
},
"products": [
{
"id": 123,
"name": "test product"
}
]
}
Using the following code I'm able to add the product, but can't update the product by it's ID.
db.users.updateOne({_id: ObjectId('5fc06554266266edf5643231')}, {
'$addToSet': {
'products': {
'id': 123,
'name': 'foobar'
}
}
}, {upsert:true})
Well, i will recommend to you to re-think your schema.
Maybe you need to create a collection called Products and in the user collection put all the id's of the products on a field called product. Then you can create a query with a populate function. Something like this
const user = await User.find(query, options)
.then(async (result) => result)
.catch((err) => logger.info(err));
const populateProducts = await Products.populate(user, populateProductsOptions)
.then(async (data) => data)
.catch((err) => logger.info(err));
But if you don't want to modify your schema you have to do several querys. There is no way to do this in single query. You have to search the document first:
If document exist:
db.users.updateOne( {_id: 123456 , yourQueryProduct ,
false ,
true);
db.users.updateOne( {_id: 123456: 123456 } ,
{$pull: {"items" : {'item_name' : "my_item_two" , 'price' : 1 }} } ,
false ,
true);
db.users.updateOne( {_id: 123456: 123456 } ,
{$addToSet : {"items" : {'item_name' : "my_item_two" , 'price' : 1 }} } ,
false ,
true);
else if document don't exist you can addToSet directly
The issue is that on this line: db.users.updateOne({_id: "5fc06554266266edf5643231"}
The _id field is being interpreted as a String, but the document you want to update saves its _id as an ObjectId. All you have to do it make it an ObjectId is this:
db.users.updateOne({_id: ObjectId("5fc06554266266edf5643231")}, {
'$addToSet': {
'products': {
'id': 123,
'name': 'foobar'
}
}
}, {upsert:true})

Finding documents with $in, using an array of another document

I'm trying to built a friendship system with Meteor.js, Blaze and Mongodb. I'm at the point where I want to display the friends of a user on his profile page.
I have the collection "users" that has the field "friends" which is the array the names of other uses can be pushed in or pulled out.
Simplified example of a user document:
"name" : "bob"
"friends" : ["value of name-field of user-document1", "value of name-field of user-document2", "etc."]
I tried to put this friends array inside a hidden input on the profile and use it from there to create a iteration on the profile-template.helpers:
<input id="friends" type="hidden" value="{{user.friends}}">
friends() {
var friends = document.getElementById("friends").value;
var friendsArray = friends.split(",");
return Users.find({name:{$in: friendsArray},})
},
But thats not how it works. How can I use/get this field, that contains the friends-array for this $in operation to get the other users that are friends with bob? I also tried this.friends But that only seems to work for iterations.
I could use
{{#each friend in user.friends}}
{{friend}}
{{/each}}
to get the names of the friends at least but I wanted to include the little avatar of those friends on the display-friends section as well, so that wouldn't do.
Edit: The user variable is defined in the Template.user.helpers as:
user: ()=> {
var user = FlowRouter.getParam('user');
return Users.findOne({name: user});
},
I tried to use this.data.char.friends inside the friends-function like so
friends() {
return Users.find({
name: { $in: (this.data.user.friends) }
});
},
But it gave me an console.log error:
Exception in template helper: ReferenceError: user is not defined
Try this aggregation query, rather than doing multiple calls to DB for fetching required data :
db.getCollection('Users').aggregate([{ $match: { name: 'bob' } },
{
$graphLookup: {
from: "Users",
startWith: "$friends",
connectFromField: "friends",
connectToField: "name",
as: "friendsDetails"
}
}, { $project: { name: 1, friends: 1, friendsDetails: { $filter: { input: '$friendsDetails', as: 'item', cond: { $ne: ['$$item.name', 'bob'] } } } } }])
Reason why we've $project and $filter is by default $graphLookup will return the requested User as well in list of friendsDetails, used these to remove it from end result.
Ah no, it was actually more simple than that.
friends() {
var name = FlowRouter.getParam('user');
return Users.find( { friends: { $in: [name] } } );
},
This one displays the friends on the profile just like that. I just have to figure out why it does that ...

Mongo find() returns [object Object]

In meteor I've created a database array. The following is the contents:
meteor:PRIMARY> db.games.find()
{ "_id" : "ceg9JJ3u5abwqeyk7", "board" : [ [ 0, 0, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] ] }
In my client/main.js file inside my template helper I have:
Template.hello.helpers({
counter() {
return Template.instance().counter.get();
},
game() {
return {
game: Games.find()}
}
});
In my main.html I have the following template:
<template name="hello">
{{game}}
</template>
My output on the browser is:
[object Object]
But I want to have the contents of my array (e.g. "0" ) put in the browser not "object".
How do I do this?
You are not properly iterating over your query.
Template.hello.helpers({
game() {
return Games.find()
}
});
main.html:
<template name="hello">
{{#each game}}
Boards: {{board}}
{{/each}}
</template>
Explanation based on your comment:
There are 3 different ways to find records from your collection.
Collection.findOne(): Returns only 1 record as an object
Collection.find().fetch(): Returns all the records as an array of objects
Collection.find(): Returns a cursor (which is a function)
Please use your browser console to see the difference between each of these using below statements to get a better understanding:
console.log(Games.findOne());
console.log(Games.find().fetch());
console.log(Games.find());
All of these will return you your entire board field data as all the data is stored as a single record.
So you have to store this data in one of the following ways in-order for you to filter the data as per your requirement.
Method 1:
Store your data in the below format as individual record for each game:
{
"_id" : "ceg9JJ3u5abwqeyk7",
"name": "Game-1",
"score": [ 0, 0, 0 ]
},{
"_id" : "bzv778zv6qge7xc8",
"name": "Game-3",
"score": [ 0, 0, 0 ]
},{
"_id" : "eji3ds9jo8yhs7739",
"name": "Game-3",
"score": [ 0, 0, 0 ]
},
You can now display the data using below code:
<template name="hello">
{{#each game}}
Game Name: {{name}}, Score: {{score}} <br/>
{{/each}}
</template>
If you want to display only the "Game-3" score, the you can filter while fetching the data using find:
<template name="hello">
{{#each game}}
Game Name: Game-3, Score: {{score}}
{{/each}}
</template>
Template.hello.helpers({
game() {
return Games.find({name:"Game-3"});
}
});
Method 2:
Store your data in the below format:
{
"_id" : "ceg9JJ3u5abwqeyk7",
"board" : [
{
"name": "Game-1",
"score": [ 0, 0, 0 ]
},
{
"name": "Game-2",
"score": [ 0, 0, 0 ]
},
{
"name": "Game-3",
"score": [ 0, 0, 0 ]
},
]
}
You can display the data using below code:
<template name="hello">
{{#each game}}
Boards: <br/>
{{#each board}}
Game Name: {{name}}, Score: {{score}} <br/>
{{/each}}
{{/each}}
</template>
If you want to display only the "Game-3" score, the you can filter using a helper as shown below:
<template name="hello">
{{#each game}}
Boards: <br/>
{{#each board}}
{{#if isThirdGame}}
Game Name: Game-3, Score: {{score}}
{{/if}}
{{/each}}
{{/each}}
</template>
Template.hello.helpers({
game() {
return Games.find()
},
isThirdGame() {
return this.name === "Game-3" ? true : false;
},
});

Sorting a Mongo cursor such that certain documents Ids come first

I have a Mongo collection that I need to search, returning the results sorted as follows:
first by whether or not the document id is present in an array that I pass in.
then by another field in the document.
My use-case is that the user has a set of 'favourited' items that I wish to display first in the list. I'm storing that list of ids in the user's document.
Is there a way to handle this?
As #Challet suggested above, with the addition of templating to avoid having to concatenate the two search results and keeping both lists reactive in the process:
html:
<template name="myTemplate">
{{#each favorites}}
{{> detail}}
{{/each}}
{{#each nonFavorites}}
{{> detail}}
{{/each}}
</template>
<template name="detail">
... layout details here ...
</template>
js:
var idsOfFavorites = []; // however you maintain this
Template.myTemplate.helpers({
favorites: function(){
return MyCollection.find({ _id: { $in: idsOfFavorites }},
{ sort: { createdAt: -1 }}); // sorting by createdAt but could be any field
},
nonFavorites: function(){
return MyCollection.find({ _id: { $nin: idsOfFavorites }},
{ sort: { createdAt: -1 }});
}
});

How do I implement Mongo DB query to find key in all objects inside an array?

After looking into several books, the mongo db reference and other stackoverflow questions, I can't seem to get this query right.
I have the following data structure:
Posts collection:
{
"content" : "...",
"comments" : [
{
"author" : "joe",
"score" : 3,
"comment" : "nice post"
},
{
"author" : "bob",
"score" : 5,
"comment" : "nice"
}]
}
What I'm trying to get is all of the author names inside each of the objects of the array inside a Handlebars helper, not in the console. So I would have something like:
...
commentsAuthors: function() {
return Collection.find({...});
}
UPDATE:
I decided to reduce my array to only an Array of strings which I later queried like this:
New Array:
{
"content" : "...",
"comments" : ["value1", "value2", "..."]
}
MeteorJS Handlebars helper:
Template.courseEdit.helpers({
comments: function(){
var cursor = Courses.find({"_id": "8mBATtGyyWsGr2PwW"}, {"comments": 1, "_id": 0}).fetch();
return cursor.map(function(doc, index, cursor){
var comment = doc.comments;
console.log(comment);
return comment;
});
}
});
At this point, I'm able to render the Whole array in my view with this {{#each}}...{{/each}}:
<ul class="list-unstyled">
{{#each comments}}
<li class="pl-14"><i class="icon-checkmark"></i> {{this}}</li>
{{/each}}
</ul>
However I'm getting the whole array in one single list item.
How can I create a separate list item for each of my Array strings?
Thanks in advance.
Should be as easy as (replace posts with your collection name):
db.posts.distinct("comments.author")
Or if you need it per post:
db.posts.aggregate([
{ $unwind: "$comments" },
{ $group: { "_id": "$_id", authors: { "$addToSet": "$comments.author"} } }
])
You can get only certain fields by limiting fields
db.collection.find({"content" : "..."},{"comments.author":1})