Mongoose scheme design many-to-many - mongodb

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.

Related

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.

Is it possible to populate nested references in Mongoose?

I'm new to MongoDB, trying to populate data from another collection into a response. The simplest example would be as follows:
const CartSchema = new Schema({
items: [{
product: { type: Schema.Types.ObjectId, ref: 'product' },
qty: Number
}],
...
});
I'm able to use .populate() when the relationships are at the root level, but in the above example, I have an array of items with their own properties, e.g. qty, plus an _id reference to a product. I would like to populate the product object into each car item, but can't seem to find any examples on what's the "right" way to do it.
Cart.findById(id)
.populate('products') // <-- something like this
.then(record => ... )
.catch(next);
I know that I could probably do a separate .find() on the products collection after locating the cart record and manually extend the initial object, but I was hoping there was a way to populate the data within the original query?
You can try this, it will work for you.
Cart.findById(id)
.populate('items.product')
.then(record => ... )
.catch(next);
.populate('items.product') will populate the product object of all the cart item present in the array.

return specific properties of array element - MongoDB/ Meteor

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

Merging two Mongoose query results, without turning them into JSON

I have two Mongoose model schemas setup so that the child documents reference the parent documents, as opposed to the Parent documents having an array of Children documents. (Its like this due to the 16MB size limit restriction on documents, I didnt want to limit the amount of relationships between Parent/Child docs):
// Parent Model Schema
const parentSchema = new Schema({
name: Schema.Types.String
})
// Child Model Schema
const childSchema = new Schema({
name: Schema.Types.String,
_partition: {
type: Schema.Types.ObjectId,
ref: 'Parent'
}
})
I want to create a static method that I can query for a Parent document, then query for any Child documents that match the parent document, then create a new item in the parent document that will reference the Children array.
Basically if the Parent document is:
{
_id: ObjectId('56ba258a98f0767514d0ee0b'),
name: 'Foo'
}
And the child documents are:
[
{
_id: ObjectId('56b9b6a86ea3a0d012bdd062'),
name: 'Name A',
_partition: ObjectId('56ba258a98f0767514d0ee0b')
},{
_id: ObjectId('56ba7e9820accb40239baedf'),
name: 'Name B',
_partition: ObjectId('56ba258a98f0767514d0ee0b')
}
]
Then id be looking to have something like:
{
_id: ObjectId('56ba258a98f0767514d0ee0b'),
name: 'Foo',
children: [
{
_id: ObjectId('56b9b6a86ea3a0d012bdd062'),
name: 'Name A',
_partition: ObjectId('56ba258a98f0767514d0ee0b')
},{
_id: ObjectId('56ba7e9820accb40239baedf'),
name: 'Name B',
_partition: ObjectId('56ba258a98f0767514d0ee0b')
}
]
}
Also, I want them to remain Mongoose documents, so I can update the Parents and Assets if I need to.
I was able to accomplish this by using toJSON on the Parent, then creating a new item that would hold the Child docs, but then obviously the Parent document isn't a real document..
The error I kept getting when I tried this was that I couldnt create a new element in the Document that wasnt in the schema.
I know I could do something like create a Virtual item that would return the promise that would query the Children, but im looking to have one static method, that returns one response (meaning they dont have to handle the virtual item as a promise or callback)
Let me know if this is possible. Thanks!
FYI, this apparently isn't possible, I've tried a few things and it doesn't seem like it will work.
Use the .populate() method of the schema.
Parent.find(query)
.populate("children")
.exec((err, items) => {
if (err) {
...
}
...
});

MongoDB: Get all mentioned items

I've got two relations in my Mongoose/MongoDB-Application:
USER:
{
name: String,
items: [{ type: mongoose.Schema.ObjectId, ref: 'Spot' }]
}
and
ITEM
{
title: String,
price: Number
}
As you can see, my user-collection containing a "has-many"-relation to the item-collection.
I'm wondering how to get all Items which are mentioned in the items-field of on specific user.
Guess its very common question, but I haven't found any solution on my own in the Docs or elsewhere. Can anybody help me with that?
If you are Storing items reference in user collection,then fetch all items from user,it will give you a array of object ids of items and then you can access all items bases on their ids
var itemIdsArray = User.items;
Item.find({
'_id': { $in: itemIdsArray}
}, function(err, docs){
console.log(docs);
});
You can get the items at the same time you query for the user, by using Mongoose's support for population:
User.findOne({_id: userId}).populate('items').exec(function(err, user) {
// user.items contains the referenced docs instead of just the ObjectIds
});