Sorting a Mongo cursor such that certain documents Ids come first - mongodb

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 }});
}
});

Related

How to use handlebars to output nested mongodb docs

I have a mongodb called categories and structured as:
{ (id), name, tree }
and data:
{ (639...bc78), "ABC", null},
{ (63d...c891), "DEF", null},
{ (63f...718d), "yyy", ",ABC,"}
'yyy' is a child of 'ABC'.
I can output ALL the data using handlebars
{{#each categories}}
{{#if tree}}
<tr><td> </td><td> </td><td>TREE:{{tree}}</td></tr>
{{else}}
<tr><td>{{_id}}</td><td>{{name}}</td></tr>
{{/if}}
{{/each}}
This outputs:
639...bc78 ABC
63d...c891 DEF
63f...718d yyy TREE:ABC
What I would like to achieve is interleaved output of the child docs (i.e. 'yyy' outputs after its parent 'ABC'):
639...bc78 ABC
TREE: yyy
63d...c891 DEF
I am fairly new to Express and very new to express-handlebars and cannot find any documentation or suggestions that would provide this functionality. Any ideas/code would be greatly appreciated.
It will be challenging to get the output you want by performing the logic in the Handlebars template. You would need to write a custom helper and that helper would need to do a lot of looping because in each iteration of the outer category loop it would need to do another category loop to find the children.
The better approach would be to map your data to a different data structure before you pass it to your template. In this structure, the top-level category objects would "own" their own arrays of children. For example:
const categoriesByName = categories.reduce((acc, category) => {
if (!category.tree) {
acc[category.name] = { ...category, children: [] };
} else {
const parent = category.tree.replace(/,/g, '');
if (acc[parent]) {
acc[parent].children.push(category);
} else {
// Note: We are assuming that children _always_ exist higher
// in the array than their parents.
// If no parent name has been indexed, we lose the child.
console.error(`No parent with name: ${name}`);
}
}
return acc;
}, {});
This will produce a structure like:
{
"ABC": {
"_id": "639...bc78",
"name": "ABC",
"tree": null,
"children": [
{
"_id": "63f...718d",
"name": "yyy",
"tree": ",ABC,"
}
]
},
"DEF": {
"_id": "63d...c891",
"name": "DEF",
"tree": null,
"children": []
}
}
Note: This structure does not get deeper than two-levels - ie., it doesn't allow children to have children.
With this structure, our template needs to iterate through the top-level categories and, for each, include an inner-loop for any children they might have:
{{#each categories}}
<tr>
<td>{{_id}}</td>
<td>{{name}}</td>
</tr>
{{#each children}}
<tr>
<td></td>
<td>TREE: {{name}}</td>
</tr>
{{/each}}
{{/each}}
Here is an example fiddle.

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 ...

Mongoose Query to Find Unique Values

In my MongoDB backend I want to create an endpoint that returns all the unique values for a property called department. Now, if I were doing this in IntelliShell I would just do something like:
db.staffmembers.distinct( "department" )
This will return an array of all the values for department.
But how do you return all unique values within a Mongoose find() query like this one?
Staffmember.find({ name: 'john', age: { $gte: 18 }});
In other words, what would the syntax look like if I want to use a find() like above, to return all unique values for department within the "staffmembers" collection?
You can use .aggregate() and pass your condition into $match stage and then use $addToSet within $group to get unique values.
let result = await Staffmember.aggregate([
{ $match: { name: 'john', age: { $gte: 18 }} },
{ $group: { _id: null, departments: { $addToSet: "$department" } } }
]);
We can use find and distinct like this for the above scenario. Aggregate might be a little overkill. I have tried both the solutions below.
Staffmember.find({name: 'john', age: {$gte: 18}}).distinct('department',
function(error, departments) {
// departments is an array of all unique department names
}
);
Staffmember.distinct('department', {name:'john', age:{ $gte: 18 }},
function(error, departments) {
// departments is an array of all unique department names
}
);
This link just nails it with all different possibilities:
How do I query for distinct values in Mongoose?

Aggregate query with no $match

I have a collection in which unique documents from a different collection can appear over and over again (in example below item), depending on how much a user shares them. I want to create an aggregate query which finds the most shared documents. There is no $match necessary because I'm not matching a certain criteria, I'm just querying the most shared. Right now I have:
db.stories.aggregate(
{
$group: {
_id:'item.id',
'item': {
$first: '$item'
},
'total': {
$sum: 1
}
}
}
);
However this only returns 1 result. It occurs to me I might just need to do a simple find query, but I want the results aggregated, so that each result has the item and total is how many times it's appeared in the collection.
Example of a document in the stories collection:
{
_id: ObjectId('...'),
user: {
id: ObjectId('...'),
propertyA: ...,
propertyB: ...,
etc
},
item: {
id: ObjectId('...'),
propertyA: ...,
propertyB: ...,
etc
}
}
users and items each have their own collections as well.
Change the line
_id:'item.id'
to
_id:'$item.id'
Currently you group by the constant 'item.id' and therefore you only get one document as result.

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

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.