Looking for MongoDB schema suggestions - mongodb

I'm relatively new to MongoDB and I'm wondering what the "best" solution would be for handling the following type of data in my database.
Collections:
Users
Lessons
Overview:
Note: Think of the implementation to be similiar to Duolingo or some other learning site.
Users have all the standard generic user information but they also need to track how many points, words learned, percentage complete, and so on, that they have for each given lesson they've started.
Lessons have data like: words learned, total points, words to review, etc.
Picture
Example of what a user would see in the widget when they view a lesson they've already started...
So what's a good way to handle this? I feel that I will need at a minimum a collection for users and lessons, but where would I store that user specific data relating to lessons they've taken?
What I've thought of so far ...
// user collection
const UserSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
firstName: { type: String },
lastName: { type: String },
gender: { type: String, enum: ['male', 'female', 'other'] },
admin: Boolean,
address: {
street: String,
city: String,
state: {
type: String,
uppercase: true,
required: true,
enum: statesArray,
},
zip: Number,
},
});
// lesson_results collection
const LessonResultsSchema = new mongoose.Schema({
user_id: Schema.Types.ObjectId,
results: [
{
id: Number,
lesson_id: Schema.Types.ObjectId,
pointsEarned: Number,
wordsLearned: Number,
wordsToPractice: [
{
id: Number,
correct: Number,
incorrect: Number,
bookmarked: Boolean,
},
],
},
],
});
// lesson collection
const LessonSchema = new mongoose.Schema({
title: String,
description: String,
totalPoints: Number,
totalWords: Number,
words: [
{
id: Number,
word: String,
points: Number
}
]
})
// if logged in
// find user in user collection
// find user's results in lessonsResults collection by using user_id
// iterate over each result to find their lesson & lesson's data
// combine data and send to frontend
// calculate percentage complete + other calculations
// render aggregate data on screen
I'm curious how you would resolve this.
Apologies if it's a bit long-winded! (+ hopefully the examples make sense)

You need to have the reference of user in LessonSchema, so that you can track how many lessons are taken by a user.
const LessonSchema = new mongoose.Schema({
user:{type:mongoose.Schema.Types.ObjectId,ref:'UserSchema',required:true},
title: String,
description: String,
totalPoints: Number,
totalWords: Number,
words: [
{
id: Number,
word: String,
points: Number
}
]
})
And you need to maintain a FLAG in LessonResultsSchema to track whether a user has completed that lesson or not. Once the lesson gets completed, you need to update the pointsEarned so that you can have the updated points data.
const LessonResultsSchema = new mongoose.Schema({
user_id: Schema.Types.ObjectId,
completed:{type:Boolean, default:0},
results: [
{
id: Number,
lesson_id: Schema.Types.ObjectId,
pointsEarned: Number,
wordsLearned: Number,
wordsToPractice: [
{
id: Number,
correct: Number,
incorrect: Number,
bookmarked: Boolean,
},
],
},
],
});

Related

How to navigate through documents in a circuler linked list fashion in MongoDB?

I have a really simple User model.
const userSchema = mongoose.Schema({
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
},
password: {
type: String,
required: true,
},
address: {
type: String,
default: null,
},
description: {
type: String,
default: null,
},
active: {
type: Boolean,
default: true,
},
interests: [
{
type: String,
default: null,
},
],
favorites: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
default: null,
},
],
});
I have a use-case where the client should be able to navigate through the users one by one in a circular fashion. I was able to implement the functionality for getting the next and previous user relative to a specific user through the
this answer.
However, the issue with this is that if I try to get the previous document on the first document in the Users collection, it of course returns an empty object. And same is the case when I try to get the next document on the last document.
I want to tweak it so that if I try to get the previous document on the first document, it would return me the last document, and if I try to get the next document on the last document, it would return me the first document. Just like how you traverse through a circular linked list. Any help would be appreciated! Thanks.

Mongoose Object Array won't work on Postman

After completing a course on MEAN Stack, I'm working on making my own webapp - a recipe page. I've designed my models, but when I try to fill the database with a new Recipe it just won't work. The main issue is the way I want to store ingredients, they'll be stored on one of the tables, then each recipe has its list of ingredients and amounts for each. For example, to make some toast you need, say "50 grs. of Butter and 4 slices of Bread". This is the Schema I'm using:
var RecipeSchema = Schema({
name: String,
desc: String,
author: { type: Schema.ObjectId, ref: 'User' },
category: { type: Schema.ObjectId, ref: 'Category' },
ingredients: [{ amount: String, ingredient: { type: Schema.ObjectId, ref: 'Ingredient' }}],
steps: [String],
image: String,
thumbnail: String,
portions: Number,
difficulty: Number,
cookingTime: Number,
comment: String
});
When I go in Postman and try to fill the "Toast Recipe" entry, I have the following:
name:Toast
desc:Toast is a common breakfast staple.
author:5cad791a7b2e651f7803f5de
category:5cb1ff8f484a172984178a97
ingredients:[{"amount": "4 slices", "ingredient": "5cb1ffdb484a172984178a98"}, {"amount": "35 grs.", "ingredient": "5cb2000d484a172984178a99"}]
steps:['Toast the bread in the oven.','Spread some butter on each toast.']
image:'null'
thumbnail:'null'
portions:1
difficulty:1
cookingTime:15
comment:'null'
But I keep getting a "cast Array" error. What could be the issue? Is it a problem with my model, with Postman, or with the way I'm sending the Array?
EDIT:
Apparently it was a problem whit the way I posted my arrays in Postman. After some more tests I managed to upload a full recipe in JSON format. The structure is correct (but what Juan suggests can be used too, to make the code cleaner).
I'am not completely sure, but when I worked with mongoose, inner objects had a new Schema object inside, in this case it would on ingredients
const IngredientSchema = new Schema({
amount: String,
ingredient: { type: Schema.ObjectId, ref:'Ingredient' }
});
const RecipeSchema = new Schema({
name: String,
desc: String,
author: { type: Schema.ObjectId, ref: 'User' },
category: { type: Schema.ObjectId, ref: 'Category' },
ingredients: [ IngredientSchema ],
steps: [String],
image: String,
thumbnail: String,
portions: Number,
difficulty: Number,
cookingTime: Number,
comment: String
});

Populate without _id

I have 2 mongoose schemas, one for stock (info about stock) and one for trade. Where trade represents the trades of a stock (so time, volume, etc). Each stock has a symbol code and the data feed that I get the trades from includes the symbol codes as strings. How would I populate these two collections since I can't use the regular mongoose 'ref' here.
Here are my two schemas:
const stockSchema = new Schema({
symbolCode: { type: String, trim: true },
symbol: { type: String, trim: true },
type: { type: String, index: true, trim: true },
country: { type: String, lowercase: true }
})
const tradeSchema = new Schema({
symbolCode: { type: String, index: true },
symbol: { type: String, index: true },
price: Number,
volume: Number,
time: Date,
currency: { type: String, default: 'USD', uppercase: true, index: true }
})
I want to remove the first two fields in the trade schema so that I can just have some kind of reference to the stock here. How can I do this?
use the populate like this:
MyModel.populate([{ path: 'author', select: 'username name -_id' }]);
the -fieldName or in your case -_id will deduct it from the projection.
For future reference, I solved this using populate virtuals as follows:
stockSchema.virtual('trades', {
ref: 'Trade',
localField: 'symbolCode',
foreignField: 'symbolCode',
justOne: true
})

MongoDB schema: Like a Comment OR a Post

I would like to setup a "like" system in my app. User should be able to like either Posts or Comments (Comments of a Post of course). How should I design this?
Users
const userSchema = new Schema({
id: { type: String, required: true },
username: { type: String, required: true },
password: { type: String, required: true },
});
Posts
const postSchema = new Schema({
content: { type: String, required: true },
authorId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }
});
Comments
const commentSchema = new Schema({
content: { type: String, required: true },
authorId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
postId: { type: mongoose.Schema.Types.ObjectId, ref: "Post", required: true },
});
Likes
const likeSchema = new Schema({
content: { type: String, required: false },
authorId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
postId: { type: mongoose.Schema.Types.ObjectId, ref: "Post", required: function() { return this.commentId? false : true } },
commentId: { type: mongoose.Schema.Types.ObjectId, ref: "Comment", required: function() { return this.postId? false : true } }
});
I'm coming from relational databases, and maybe my design is completely wrong for nosql. My main interrogation is about Likes, I have no idea how to accept likes on Posts OR Comments.
I would prefer a separate collection:
User:
id:
...
Post:
id:
userId:
...
Comment:
id:
userId:
postId:
Like:
id:
userId:
postId:
commentId:
The second one storing an array will lead you cyclic dependencies in the backend. Especially, when you use NodeJS and strict to flow.
MongoDB is powerful at storing documents. Documents hold the relations.
I would model it in the way your data is being accessed. I do recommend playing around with the powerful aggregation framework and array operators to experience the possibilities. What I would explore is the following
User:
id:
name:
picture:
...
Posts:
id:
authorid:
content:
total_views:
tags: array of String
likes: array of Likes {[
liked_by: user_id
],...}
comments: array of Comments {[
author_id: ...
comment: ...
reactions: array of Comments {[],...}
likes: array of Likes {[
liked_by: user_id
],...}
],...}
Will this model scale? Documents can hold 16MB of data. 16MB in textual format is HUGE.
PS please think again on storing username/password in the database. This is a whole other discussion. Look into the topics of authentication, authorisation, OAuth, hashing/salting etc.
post={
...keys,
likes:[likeSchema],
comments:[CommentSchema]
}
this is i prefer, even if you want to store recursive comments just use
commentschema={
id:unique commet id
text:...
user_id:who wrote this comment
parent_id: to which this comment belongs to!
depth: comment depth as your wish (mostly 2)
}
parent id will be null for a comment posted directly on post
parent id will be comment_id of the comment to which this comment posted for. if its a recursive comment.
hope you get it.
Since, the question is about schema for like a comment or post. I'll focus on likes.
Build a schema like this. Here targetId will be postId or commentId.
const likeSchema = new Schema({
content: { type: String, required: false },
authorId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
targetId: { type: mongoose.Schema.Types.ObjectId, ref: "Post", required: function() { return this.commentId? false : true } }
});
Some points you need to consider:
Store likes of posts in post collection
Store likes of comments in comments collection
You need to build a mechanism to calculate likes and store in that collection

Products with different variants schema

I am trying to create an e-commerce website using MongoDB. I have created a Product and variant model, my question is how can I search the product with variant, for example for "Size" user can add variant value as "S" or "Small". How can I search the product which has for example small product in this case as a product have many variants, how can I list eg. all products with small size. Here is my variant model.
var variantSchema = Schema({
name: {
type: String,
required: true,
unique: true
},
count: {type: Number, default : 0}
});
And my Product Schema is:
var productSchema = Schema({
sku: {
type: String,
lowercase: true
}, //, required: true, unique: true
name: {
type: String,
lowercase: true,
max: 65,
required: true
},
slug: {
type: String,
lowercase: true,
unique: true,
index: true,
slug: "name",
slug_padding_size: 3
},
status: Boolean,
listPrice: Number,
description: {
short: {
type: String,
trim: true,
lowercase: true
},
long: {
type: String,
trim: true,
lowercase: true
}
},
images: [],
categoryId: {
type: Schema.Types.ObjectId,
ref: 'Category'
},
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
},
isActive: Boolean,
vars: [
{
varId : {
type: Schema.Types.ObjectId,
ref: 'Variants'
},
values: [
{
value : String,
image:[]
}
]
}
]
});
Based on your comment.
You can distinguish "Small" and "small" by ignoring case sensitive.
UserModel.findOne({
email: {'$regex': "^"+ email +"$", $options:'i'}
}, function (err, data) {
callback(err, data)
});
But you can not match S with Small.
Approach 1:
You need to maintain the possible words that you want to consider as Small. Maybe by inserting in Variant Schema an array ["S", "Small"] like this. But in this scenario. You must have to caution about S. S can be anything. (I am not recommended this approach)
Approach 2:
I would like to suggest making one schema (SizeSchema) that can present the size. for e.g. Small, Large, Extra small, Extra Large etc... And reference that SizeSchema to VariantSchema and ProductSchema to VariantSchema. (Triple relationship). And this would be fixed for the end user. No one will have an option like "S".
Hope this may help you.