return specific properties of array element - MongoDB/ Meteor - mongodb

I have documents in games collection.Each document is responsible for holding the data that requires to run the game. Here's my document structure
{
_id: 'xxx',
players: [
user:{} // Meteor.users object
hand:[] //array
scores:[]
calls:[]
],
table:[],
status: 'some string'
}
Basically this is a structure of my card game(call-bridge). Now what I want for the publication is that the player will have his hand data in his browser( minimongo ) along with other players user, scores, calls fields. So the subscription that goes down to the browser will be like this.
{
_id: 'xxx',
players: [
{
user:{} // Meteor.users object
hand:[] //array
scores:[]
calls:[]
},
{
user:{} // Meteor.users object
scores:[]
calls:[]
},
// 2 more player's data, similar to 2nd player's data
],
table:[],
status: 'some string'
}
players.user object has an _id property which differentiates the user. and in the meteor publish method, we have access to this.userId which returns the userId who is requesting the data.It means I want the nested hand array of that user whose _id matches with this.userId. I hope this explanations help you write more accurate solution.

What you need to do is "normalize" your collection. Instead of having hand,scores, calls in the players field in the Games collection, what you can do is create a separate collection to hold that data and use the user _id as the "Key" then only reference the user _id in the players field. For example.
Create a GameStats collection(or whichever name you want)
{
_id: '2wiowew',
userId: 1,
hand:[],
scores:[],
calls:[],
}
Then in the Games collection
{
_id: 'xxx',
players: [userId],
table:[],
status: 'some string'
}
So if you want to get the content of the current user requesting the data
GameStats.find({userId: this.userId}).hand
EDIT
They do encourage denormalization in certain situations, but in the code you posted above, array is not going to work. Here is an example from the mongoDB docs.
{
_id: ObjectId("5099803df3f4948bd2f98391"),
name: { first: "Alan", last: "Turing" },
birth: new Date('Jun 23, 1912'),
death: new Date('Jun 07, 1954'),
contribs: [ "Turing machine", "Turing test", "Turingery" ],
views : NumberLong(1250000)
}

To get a specific property from an array element you may write something as in the below line db.games.aggregate([{$unwind:"$players"},{$project:{"players.scores":1}}]); this gives us only the id and scores fields

Related

Why I have more documents in my MongoDB collection than I created

I'm creating a chat app using MERN and when I inserted a new user in my collection I got all the collection
await usersCollection.find();
and this array included not only my users:
{
_id: new ObjectId("63582710bc6f137c8532ceda"),
username: 'Пользователь 1',
img: '...long img string',
userId: 'dfy5nOU972SxsD7EB3q4ka7Z_9ZzA_LN'
},
but a lot of documents looking like this:
{
_id: new ObjectId("6358205ce63a49c2ed4b8575"),
type: 2,
uid: 'ac98be53ee7cb872',
nsp: '/'
}
And when I create 1 new user I get various number of these new objects.
I can look only for documents with fields I need:
await usersCollection.find({}, { username: 1, img: 1, userId: 1 });
But it doesn't solve my problem, these objects will be created in my collection anyway
My full code is here: https://codepen.io/yuliya-d98/pen/mdKdKrM?editors=0010
Update: according to MongoDB triggers they are creating every 5 seconds and not depending on requests.
Update 2: these objects create in every socket.io heartbeat. And if I delete mongoDB-socket.io adapters, these objects don't create.

Filter out an object with arrays having specific ids on the basis of an existing collection - (Aggregate Framework)?

I'm having two objects,
const originTimeStamp = {
chats: '2021-06-25T12:21:21.835+00:00',
users: '2021-06-21T12:21:21.835+00:00',
history: '2021-06-18T12:21:21.835+00:00'
}
const controlIds = {
chats: ['1bfe','2bfs','3bhr'],
users: ['6jkj'],
history: ['8her'],
}
and a collection that typically have some logs related to user activities:
{
controlId: '2bfs'
createdAt: '2021-07-19T12:21:21.835+00:00'
},
{
controlId: '6jkj'
createdAt: '2021-06-18T12:21:21.835+00:00'
},
{
controlId: '8her'
createdAt: '2021-06-25T12:21:21.835+00:00'
},
What I basically want to do is to filter out the controlIds object in such a way that if the control id exists in the collection and If the origin time stamp of that section say for chats '2021-06-25T12:21:21.835+00:00' < '2021-07-19T12:21:21.835+00:00' (of id '2bfs' from collection ) we will remove that id from the object.
Expected Result:
const controlIds = {
chats: ['1bfe','3bhr'],
users: ['6jkj'],
history: [],
}
Is there any way to achieve it with aggregation pipeline, right now i tried creating a flow but not able to do that? Here is the suggested flow i tried so far:
$match the documents with a particular range of timestamps
$project only the control_Id
$group them using $push to get all documents in an array
Assign this output to a variable and filter your object.

MongoDB User notifications scheme suggestions

I was wondering what is the best scheme for user/notifications kind of scenario like the following:
1 | 01-04-2020 | X | John liked your post2
2 | 01-03-2020 | X | Mike and 9 other persons liked your post1
3 | 01-02-2020 | | Rose and 2 other persons liked your post1
4 | 01-01-2020 | | Bernard liked your post1
x = notification has not been read by the user yet
post1 = the same post in all the notifications
Let's say I have a Notification collection like:
_id: ObjectID
receiver: ObjectID (User)
sender : ObjectID (User)
type: String ("post", "comment", etc...)
typeID: ObjectID (Post, Comment, etc...)
message: String
isRead : Boolean
timestamp: Date
A User collection like :
_id: ObjectID
username: String
.
.
.
email: String
A Post Collection like :
_id: ObjectID
postedBy: ObjectID (User)
.
.
.
content: String
A Like collection like :
_id: ObjectID
type: String ("post", "comment", etc...)
typeID: ObjectID (Post, Comment, etc...)
likedBy: ObjectID (User)
On the 01-01-2020, the user opened his notifications panel. Between the last time he checked his notifications and this date, only 1 person liked his post1.
On the 01-02-2020, the user opened his notifications panel. Between the last time he checked his notifications (01-01-2020) and this date, 2 persons liked his post1.
On the 01-04-2020, the user opened his notifications panel. Between the last time he checked his notifications (01-02-2020) and this date, 9 persons liked his post1 and 1 person liked his post2.
I want the user to be able to see all his previous notifications as well as the notifications he hasn't read yet. If the user has several notifications for the same post (X people liked his post since the last time he checked his notifications), I want to group them as 1 notification (I will mark all of them as read once he read that one grouped notification).
How can I do that?
Please let me know if you need more information or if I am being unclear.
Thanks
Edit:
I'm having a hard trying to figure out how to aggregate those notifications. I think I need some kind of read date marker as well to group the notifications that were grouped and read at the same time, but maybe I need another collection to store the grouped notifications?
Notification.aggregate([
{
$group: {
_id: {
typeID : "$typeID",
receiver: "$receiver",
isRead: "$isRead"
// maybe something with a read date?
},
count: {$sum: 1}
}
}
])
I guess this article at Mongo University is a relevant answer to your question.
Use at least two collections: users and notifications also, you your users _id field isn't something like name and you'll allow them to be renamed, then it's perfect to have 3-rd collection likes, instead making likes as a embedded documents in array, like this:
User's schema:
_id: ObjectID,
likes: [{
_id: ObjectID //like_id,
other: "field"
}]
Notifications:
_id: ObjectID
receiver: ObjectID (User)
sender : ObjectID (User)
type: {
type: String,
enum: ["Post", "Comment"] /** Use enum for avaliable values */
},
typeID: {
type: ObjectID, /** It's better if every ID field have an index */
index: true,
unique: true
}
message: String
isRead : Boolean
timestamp: {
type: Date,
default: Date.now /** Don't forget about default values, but check my advice below about $timestamps */
}
Not sure that timestamp field is needed for you, as for me, it's
better to use {timestamps: true} option.
Also, every field with ObjectID should be indexed, it you needed
this fields for aggregation framework. It's a perfect performance
case for $lookup
I want the user to be able to see all his previous notifications as well as the notifications he hasn't read yet.
You needed a compound index for this, like {sender:1, createdAt: -1, isRead: 1}
I want to group them as 1 notification (I will mark all of them as read once he read that one grouped notification).
This is a job for aggregation framework, via:
{
$match: { query_criteria },
},
{
$group: { query_group_by $notification.typeID }
}
So your schema is fine, it's possible to do that. By the way, to test your own queries, you could use MongoPlayground, instead of production DB.
As for the likes schema, it's for you to decide, but maybe it's better to have them as an embedded (child) documents, like:
Post
_id: ObjectID
postedBy: ObjectID (User)
likes: [{
/** Likes Sub-schema */
}]
content: String
Take a look at sub-schema pattern in mongoose.
Hope it will helps you!

Mongoose scheme design many-to-many

I'm trying to come up with a mongoose scheme architecture/design for a task management app.
Desired functionality and models:
Models:
User - has boards
Boards - has lists
Lists - has cards
Cards
Functionality:
User can create a board
User can create a list in the board
User can create a card in the list
User can add members to the board
User can add members to the card from the board members
User can delete board, deleting all the lists and cards, and removing members
User can delete list, deleting all the cards in the list
User can delete card, removing all the members from the card
User can update the list position (sort) in the board
User can update cards position (sort) in the list
Additional functionality (Optional)
User can add comments to the card
Card activity logging (On move, on edit, on comment)
I know its alot to ask, but how would go about designing a scheme for this kind of functionality?
Would child-parent references be the best solution?
Ex.
User:
{
name: 'Bob',
_id: '2626'
}
Board:
{
name: 'Board name',
_id: '123456',
members: [
{ type: ObjectID, ref: 'user' } // '2626'
]
}
List:
{
name: 'List name',
_id: '2525',
boardId: { type: ObjectID, ref: 'board' } // '123456'
}
Card:
{
name: 'Card name',
boardId: { type: ObjectID, ref: 'board' } // '123456',
listId: { type: ObjectID, ref: 'list' } // '2525'
}
How would I go about querying this type of structure?
1) Get a list of boards by users id
2) Get a list of lists by board id
3) Get a list of cards by board id & listid
So for the board view I would go and grab all the lists, but then I would have to go and get all the cards for the each list, seems not really efficient.
Perhaps when entering board view I should query just the cards by the board id, but then how do I get the lists, and put each card into its own list?
How do I handle deleting the card or moving a card from one list to another?
Don't be hard on me please, I'm really new to the mongodb world, but I'm really trying my best.
The schema you've defined is pretty good, here's how the flow would be.
Initially, when a user signs in, you'll need to show them the list of boards. This should be easy since you'll just do a find query with the user_id on the board collection.
Board.find({members: user_id}) // where user_id is the ID of the user
Now when a user clicks on a particular board, you can get the lists with the board_id, similar to the above query.
List.find({boardId: board_id}) // where board_id is the ID of the board
Similarly, you can get cards with the help of list_id and board_id.
Card.find({boardId: board_id, listId: list_id}) // where board_id is the ID of the board and listId is the Id of the list
Now, let's look at cases wherein you might need data from 2 or more collection at the same time.
For example, when a user clicks on board, you not only need the lists in the board but also the cards in that board.
In that case, you'll need to write an aggregation as such,
Board.aggregate([
// get boards which match a particular user_id
{
$match: {members: user_id}
},
// get lists that match the board_id
{
$lookup:
{
from: 'list',
localField: '_id',
foreignField: 'boardId',
as: 'lists'
}
}
])
This will return the boards, and in each board, there'll be an array of lists associated with that board. If a particular board doesn't have a list, then it'll have an empty array.
Similarly, if you want to add cards to the list and board, the aggregation query will be a bot more complex, as such,
Board.aggregate([
// get boards which match a particular user_id
{
$match: {members: user_id}
},
// get lists that match the board_id
{
$lookup:
{
from: 'list',
localField: '_id',
foreignField: 'boardId',
as: 'lists'
}
},
// get cards that match the board_id
{
$lookup:
{
from: 'card',
localField: '_id',
foreignField: 'boardId',
as: 'cards'
}
}
])
This will add an array of cards as well to the mix. Similarly, you can get cards of the lists as well.
Now, let's think about whether this is the best schema or not. I personally think the schema you suggested is pretty good because another way to go about it would be to store IDs in the parent collection, which will let you use populate to get the data instead of a lookup query.
For example, storing list ids in board collection. The downside to this is, whenever a new list is added, you need to add that list in the list collection and also update the board to which the list is connected to (add the list ID), which I think is too tedious.
Finally, some suggestion on the schema you've given,
I think you should add user_id (creator's ID) in every collection, cause there are many cases wherein you need to show the name of the user who created that particular board or list or anything else and also since you have the feature of adding users to a particular card, etc I think you should have two fields, one is creator_id and the other should be associated_users, which will be an array (obviously you can choose better names).
You should add position field in cards and other collections which you want to sort by position. This field should be a number.
Deleting a card or moving it from one list to another should be pretty easy and self-explanatory by now.
Edit 1: Based on the comment
You don't need to assign cards to the list 'after' the aggregation, you can do this in your aggregation itself, so it'll be something like this,
Board.aggregate([
// get boards which match a particular user_id
{
$match: { members: user_id }
},
// get lists that match the board_id
{
$lookup:
{
from: 'list',
localField: '_id',
foreignField: 'boardId',
as: 'lists'
}
},
// unwind lists (because it's an array at the moment)
{
$unwind: '$lists'
},
// Now you have object named lists in every board object
// get cards that match the list_id (note that the cards contain list_id)
{
$lookup:
{
from: 'card',
localField: '_id',
foreignField: 'listId',
as: 'cards'
}
},
// now merge back the objects and get a simple object for each boardID
{
$group: {
_id: "$_id",
members: { $addToSet: "$members" },
lists: { $addToSet: "$lists" }
}
}
])
This will give you something like this,
data = {
'_id': '123456',
'members': [
{
name: 'Bob',
_id: '2626'
},
{
name: 'Matthew',
_id: '2627'
}
],
'lists': [
{
name: 'List 1',
_id: '2525',
boardId: '123456',
cards: [
{
name: 'Card 1',
boardId: '123456',
listId: '2525'
},
{
name: 'Card 2',
boardId: '123456',
listId: '2525'
}
]
},
{
name: 'List 2',
_id: '2526',
boardId: '123456',
cards: [
{
name: 'Card 3',
boardId: '123456',
listId: '2525'
},
{
name: 'Card 4',
boardId: '123456',
listId: '2525'
}
]
}
]
}
So basically, you can get the list and the cards for those list in a single query itself and it is quite efficient.
Now coming to the two queries you asked for,
Card moved from one list to another, just edit the listId field in the card document to the new listID (it's quite simple actually).
Card moved up a position in a list
As I said, if you want the position you need to add a field called position in the documents and then whenever the card is moved you need to change the value of 'position' of those cards.
In the aggregation, you just need to add another stage called '$sort' and sort it according to the position value.
This is going to be a bit tedious since whenever you move a card up, you need to update the position of the card above as well.

Building a dynamic mongo query for meteor

I'm building an app that has clickable 'filters'; I'm creating a list of objects(?) that I want to pass to a mongo 'find', so that I can pull out listings if selected attributes match a certain score.
My data is structured like this (a snippet):
name: 'Entry One',
location: {
type: 'Point',
coordinates: [-5.654182,50.045414]
},
dogs: {
score: '1',
when: 'seasonal',
desc: 'Dogs allowed from October to April'
},
lifeguard: {
score: '1',
when: 'seasonal',
desc: 'A lifeguard hut is manned between April and October',
times: ''
},
cafe: {
score: '1',
name:'Lovely cafe',
open:'seasonal'
}, ...
My search variable is a list of objects (I think?) that I assign to a session variable. If I output this session var ('searchString') via JSON.stringify, it looks like this:
{"cafe":{"score":"1"},"dogs":{"score":"1"}}
I'd like to pass this to my mongo find so that it only lists entries that match these scores on these attributes, but it's returning zero results. Do I need to somehow make this an $and query?
Currently it looks like this:
Beaches.find(searchString);
Unfortunately as soon as I drop searchString into the find, I get zero results even if it's empty {}. (When it's just a find() the entries list fine, so the data itself is ok)
What am I doing wrong? I'm relatively new to mongo/meteor, so I apologise in advance if it's something stupidly obvious!
Don't stringify the query. Flatten the object instead. Example:
Beaches.find({
"cafe.score": 1,
"dogs.score": 1,
});