Building a simple news feed in node + Mongodb + Redis - node.js

My goal is to build a simple news feed in node.js with the help of mongodb and redis. It similar like twitter
So the scenario is pretty straight forward, once User A follow User B. Later on User's A News feed (Home page) will be shown User B's Activity like what he posted.
Schema for User
const UserSchema = new Schema({
email: { type: String, unique: true, lowercase: true},
});
const followSchema = new Schema(
{
user: { type: Schema.Types.ObjectId, required: true, ref: 'User' },
target: { type: Schema.Types.ObjectId, required: true, ref: 'User' },
});
Currently the design of my user's schema is pretty simple, when I follow another user, I will just create the Follow Schema Object
and there is another schema, which is post schema
/* This is similar like the Tweet */
var PostSchema = new Schema({
// Own by the user
creator: { type: Schema.Types.ObjectId, ref: 'User' }
body: String,
});
This schema is for user to post anything, similar like twitter posting.
Let say I have followed bunch of users
{
user: 'me',
target: 'draco'
},
{
user: 'me',
target: 'donald'
},
{
user: 'me',
target: 'joker'
}
and let say one of my followers, post something. How do i present it to my current news feed?
/* I'm following Joker */
app.post('/follow', (req, res, next) => {
let follow = new Follow();
follow.user = "me";
follow.target = "joker";
// Do i need to use redis to subscribe to him?
follow.save();
})
/* Joker posted something */
app.post('/tweet',(req, res, next) => {
let post = new Post();
post.creator = "joker";
post.body = "Hello my name is joker"
post.save();
// Do i need to publish it using redis so that I will get his activity?
});
Here's my attempt
app.get('/feed', function(req, res, next) {
// Where is the redis part?
User.findOne({ _id: req.user._id }, function(err, foundUser) {
// this is pretty much my attempt :(
})
})
When should I use redis to actually do the pub and sub? so that I could take the content of one of my followers and show it on my timeline?

I have built a social network which has a news feed, too. Here is how I did it.
Basically, you have 2 methods to built a newsfeed:
Fanout on write (push) method
Fanout on read (pull) method
Fanout on write
First, you will need another collection:
const Newsfeed = new mongoose.model('newsfeed', {
owner: {type: mongoose.Types.ObjectId, required: true},
post: {type: mongoose.Types.ObjectId, required: true}
});
When a user post something:
Get n follower
Push (fanout) this post to n follower
When a user get a feed:
Get from Newsfeed collection
Example:
router.post('/tweet', async (req, res, next) => {
let post = await Post.create({});
let follows = await Follow.find({target: req.user.id}).exec();
let newFeeds = follows.map(follow => {
return {
user: follow.user,
post: post.id
}
});
await Newsfeed.insertMany(newFeeds);
});
router.get('/feed', async (req, res, next) => {
let feeds = await Newsfeed.find({user: req.user.id}).exec();
});
Fanout on read
When a user post something:
Save
When a user get feed
Get n following
Get posts from n following
Example
router.post('/tweet', async (req, res, next) {
await Post.save({});
});
router.get('/feeds', async (req, res, next) {
let follows = await Follow.find({user: req.user.id}.exec();
let followings = follows.map(follow => follow.target);
let feeds = await Post.find({user: followings}).exec();
});
You don't need Redis or pub/sub to implement a newsfeed. However, in order to improve the performance, you may need Redis to implement some kind of cache for this.
For more information or advance technique, you may want to take a look at this.

User Schema :
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
const userSchema = new Schema({
name:{type:String},
email: { type: String, unique: true, lowercase: true},
},{
collection: 'User'
});
var User = module.exports = mongoose.model('User', userSchema);
Follow Schema :
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var followSchema = new Schema(
{
follow_id: { type: Schema.Types.ObjectId, required: true, ref: 'User' },
leader_id: { type: Schema.Types.ObjectId, required: true, ref: 'User' }
},{
collection:'Follow'
});
var Follow = module.exports = mongoose.model('Follow', followSchema);
Post Schema :
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var postSchema = new Schema({
creator: { type: Schema.Types.ObjectId, ref: 'User' }
body: {type: String , required:true},
created_at :{type:Date , default:Date.now}
},{
collection:'Post'
});
var Post = module.exports = mongoose.model('Post', postSchema);
Now Suppose you have 3 users in User collection :
{ _id: ObjectID('5a2ac68d1413751391111111') ,name:'John' , email:'john#gmail.com'}
{ _id: ObjectID('5a2ac68d1413751392222222') ,name:'Morgan' , email:'morgan#yahoo.com'}
{ _id: ObjectID('5a2ac68d1413751393333333') ,name:'Emily' , email:'emily#outlook.com'}
Now John Follows Morgan and Emily :
so in Follow collection there are two records
1) follow_id = John's ID and leader_id = Morgan's ID
2) follow_id = John's ID and leader_id = Emily's ID
{
_id: ObjectID('5a2ac68d141375139999999'),
follow_id : ObjectID('5a2ac68d1413751391111111'),
leader_id : ObjectID('5a2ac68d1413751392222222')
},
{
_id: ObjectID('5a2ac68d1413751393333333'),
follow_id : ObjectID('5a2ac68d1413751391111111'),
leader_id : ObjectID('5a2ac68d1413751393333333')
}
Now if you want to get User's Following :
app.get('/following/:user_id',function(req,res){
var userid=req.params.user_id;
Follow.find({follow_id:mongoose.mongo.ObjectID(userid)})
.populate('leader_id')
.exec(function(err,followings){
if(!err && followings){
return res.json({followings:followings});
}
});
});
for getting User's Followers :
app.get('/followers/:user_id',function(req,res){
var userid=req.params.user_id;
Follow.find({leader_id:mongoose.mongo.ObjectID(userid)})
.populate('follow_id')
.exec(function(err,followers){
if(!err && followers){
return res.json({followers:followers});
}
});
});
npm install redis
in your app.js :
var redis = require('redis');
var client = redis.createClient();
When one user create post :
app.post('/create_post',function(req,res){
var creator=new mongoose.mongo.ObjectID(req.body.creator);
var postbody=req.body.body;
async.waterfall([
function(callback){
// find followers of post creator
Follow.find({leader_id:creator})
.select({ "follow_id": 1,"leader_id":0,"_id": 0})
.exec(function(err,followers){
if(!err && followers){
callback(null,followers);
}
});
},
function(followers, callback){
// saving the post
var post=new Post({
creator: creator,
body: postbody
});
post.save(function(err,post){
if(!err && post){
// adding newly created post id to redis by key userid , value is postid
for(var i=0;i<followers.length;i++){
client.sadd([followers[i].follow_id,post.id]);
}
callback(null,post);
}
});
}
], function (err, result) {
if(!err && result){
return res.json({status:"success",message:"POST created"});
}
});
});
Now For Getting User NewsFeed :
1) first get array of postid from redis key of userid
2) loop through postid and get post from mongo
Function for get newsfeed by userid :
app.get('/newsfeed/:user_id',function(req,res){
var userid=req.params.user_id;
client.smembers(userid,function(err, reply) {
if(!err && reply){
console.log(reply);
if(reply.length>0){
var posts=[];
for(var i=0;i<reply.length;i++){
Post.findOne({_id:new mongoose.mongo.ObjectID(reply[i])}).populate('creator').exec(function(err,post){
posts.push(post);
});
}
return res.json({newsfeed:posts});
}else{
// No News Available in NewsFeed
}
}
});
});
Here we have use redis to store [userid,array of postids] for newsfeed ,
but if you dont want to use redis than just use below Newsfeed Model and store user_id and post_id for newly created post and then display it.
NewsFeed Schema :
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var newsFeedSchema = new Schema({
user_id: {type: Schema.Types.ObjectId, refer:'User' , required:true}
post_id: {type: Schema.Types.ObjectId, refer:'Post' , required:true},
},{
collection:'NewsFeed'
});
var NewsFeed = module.exports = mongoose.model('NewsFeed', newsFeedSchema);
Helpful link for Redis : https://www.sitepoint.com/using-redis-node-js/
for Async : https://caolan.github.io/async/docs.html#

Related

insert to MongoDB array with axios, restAPI and nodeJS

I am trying to add an item to a MongoDB array with RESTAPI through Axios. I thought it would look similar to the push method but I have no idea how to do that.
my Model is of a person:
const Schema = mongoose.Schema;
const PersonSchema = new Schema({
name: String,
password: String,
friends: [],
missions: []
})
const personModel = mongoose.model('Person', PersonSchema);
I want to add a mission to the mission array of a person.
and for example, in order to add a new Person, I use NodeJS and API:
(api.js)
router.post('/api/people', (req, res) => {
const personToAdd = req.body;
const newPersonPost = new personModel(personToAdd);
newPersonPost.save((e) => {
if (e) {
console.log("error");
}
});
res.json({
msg: 'Received'
})
});
and in the client side I use Axios:
axios({
url: 'http://localhost:8080/api/people',
method: 'POST',
data: dataToUpdate
})
.then(() => {
console.log('axios sent info to server');
}).catch((e) => {
console.log('error' + e);
})
Thank you so much!
express
router.post('updating mission endpoint url', async (req, res) =>
try {
const query = { /* content */}; /* write a query to retrieve the concerned user by using a unique identifier */
let person = await personModel.findOne(query);
person.missions.push(req.body.mission);
personModel.save();
} catch (err) {
console.log(err);
}
});
client
In the client side you just have to put the mission you want to add in data like you did above with the right endpoint url and you should add a unique identifier for the user you want to add mission to.
[] will not assign array type to your variable.
Change your schema file with the following:
const Schema = mongoose.Schema;
const PersonSchema = new Schema({
name: { type: String },
password: { type: String },
friends: { type: Array },
missions: { type: Array }
})
Update the db model entity file with following
First method:
const Schema = mongoose.Schema;
const PersonSchema = new Schema({
name: String,
password: String,
friends: {type : Array},
missions: {type : Array}
})
const personModel = mongoose.model('Person', PersonSchema);
Second Method :
const Schema = mongoose.Schema;
const PersonSchema = new Schema({
name: String,
password: String,
friends: [{ type: String }],
missions: [{ type: String }]
})
const personModel = mongoose.model('Person', PersonSchema);
You can update the array object as per your requirements.
You just want to be using the $push update operator, very simple, like so:
db.collection.updateOne(
{
_id: user._id
},
{
"$push": {
"missions": {
mission: newMission
}
}
})
Mongo Playground

mongodb one to many relationship using nodejs and express

Its been a tough time for me trying to figure this out.
The problem I'm trying to solve is this:
I have USERS who has list of COMPANIES,
this COMPNAY has list of PROJECTS etc.
I'm trying to create a PROJECT under a COMPANY and a COMPANY under a USER using referencing.
This USER is going to be populated from form fields likewise COMPANY PROJECT
USERS SCHEMA
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
//============= User Schema============//
const userSchema = new Schema({
name: String,
email: String,
//referencing user's company/companies
company: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'company',
},
],
},{
timestamps: true
});
const Users = mongoose.model('user', userSchema);
module.exports = Users;
COMPANY SCHEMA
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
//======== Company schema ========//
const compSchema = new Schema({
company_name: String,
//referencing user schema
user: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'user',
},
],
//referencing projects
projects: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'project',
},
],
},{
timestamps: true
});
const Company = mongoose.model('company', compSchema);
module.exports = Company;
PROJECT SCHEMA
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
//PROJECT SCHEMA
const projectSchema = new Schema({
project_title: String,
//referencing company schema
company: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'company',
}
],
},{
timestamps: true
});
const Projects = mongoose.model('project', projectSchema);
module.exports = Projects;
CONTROLLERS
const Users = require('./userSchema');
const Company = require('./companySchema');
const Projects = require('./projectSchema');
//create user
exports.createUser = async (req, res) => {
const data = {
name: req.body.name,
email: req.body.email,
}
Users.create(data, (err, done) => {
if(err) return err;
res.json({UserData: done});
return;
});
}
exports.getAllUsers = async(req, res) => {
}
//create company
exports.createCompany = async (req, res) => {
const CompanyData = {
company_name: req.body.company_name,
}
Company.create(companyData, (err, done) => {
if(err) return err;
res.json({CompanyDetails: done});
return;
});
}
exports.getllCompany = async (req, res) => {
}
//create project
exports.createProject = async (req, res) => {
const projectData = {
project_title: req.body.project_title,
}
Projects.create(projectData, (err, done) => {
if(err) return err;
res.json({ProjectDetails: done});
return;
});
}
exports.getAllProjects = async (req, res) => {
}
I understand that populate can be used to push documents to children. Any assistance offered will be highly appreciated
Store reference
Firstly, while creating a Company inside createCompany controller, you should add user _id in users array like this:
const CompanyData = {
company_name: req.body.company_name,
users: [req.body.userId] // Lets assume, user _id is coming in request body
}
Company.create(companyData, (err, done) => {
// Your code
});
Then, when you create a Project, add _id of Company you just created, in createProject controller like this:
const projectData = {
project_title: req.body.project_title,
company: [req.body.comapnyId] // Again assuming, company _id is coming in request body
}
Projects.create(projectData, (err, done) => {
// Your code
});
Update reference
Also, as I see in your schemas, you are trying to keep it bidirectional, like user's _ids in company doc & company's _ids in user doc.
To achieve this, if you create a user, you should update the corresponding company with pushing(addToSet actually) new user _id in user array.
Example:
To create a user in controller, run two scripts synchronously one-by-one:
Create a user with company _id and other details.
const data = {
name: req.body.name,
email: req.body.email,
company: [req.body.companyId]
}
Users.create(data, (err, done) => {
// Your code
});
Update company doc(whose _id is saved in above user) with _id of newly created user.
const updateCompany = { "$addToSet": { user: user._id } }; // Get `user._id` from above synchronous script
Company.update({_id: req.body.companyId}, data, (err, done) => { // Updated filter
// Your code
});

How can you render items only if matching category?

I'm trying to build an ecommerce shop using express and mongodb. I'm trying to make categories for the shop. (e.g when someone clicks a category it should only display the items corresponding to that category) I've tried multiple ways to tackle this issue but haven't found a solution.
My current code is this one:
var NFTitem = require("../models/NFTitem")
var Item = require('../models/item');
var async = require('async')
exports.item_list = function(req, res, next) {
async.parallel({
item: function(callback) {
Item.find({}).exec(callback);
},
collection_list: function (callback) {
NFTitem.find({}).exec(callback);
},
collection: function(callback) {
NFTitem.find({'_id' : req.params.id }).exec(callback)
},
thisCategory: function (callback) {
Item.find({'collectionItem' : req.params.id }).exec(callback);
},
}, function(err, results) {
res.render('item_list.jade', { title: "NFT Marketplace", collection_list: results.item});
})
}
Now the logic behind this is to find the items corresponding to the items database which is (Item) and extract it, finding only the id matching the categories database. Then we would find the id for the categories database which is (NFTitem). Then we would do and if else command so that it only shows if both ids are matching. But this method doesn't seem to work.
I've also tried filtering the thing but it displays nothing when i filter it out. I would like to know what would be the best solution for this and if there's any way I could solve this issue.
rendered website:
block content
h1= title
ul
each collection in collection_list
li
a(href=collection.url) #{collection.name}
models for items:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var ItemSchema = new Schema(
{
name: {type: String, required: true},
description: {type: String, required: true},
collectionItem: {type: Schema.Types.ObjectId, ref: 'nftitem', required: true},
price: {type: Number, required: true},
instock: {type: Number, required: true},
}
);
// Virtual for book's URL
ItemSchema
.virtual('url')
.get(function () {
return '/catalog/item/' + this._id;
});
//Export model
module.exports = mongoose.model('Item', ItemSchema);
models for collections:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var NFTItemSchema = new Schema(
{
name: {type: String, required: true},
description: {type: String, required: true},
}
);
// Virtual for book's URL
NFTItemSchema
.virtual('url')
.get(function () {
return '/catalog/nftitem/' + this.name;
});
//Export model
module.exports = mongoose.model('nftitem', NFTItemSchema);
Since the collectionItem field in Item Schema is a reference to NFTItemSchema Schema, you can query Item Collection directly. You can refactor and simplify your code like this:
const Item = require('../models/item');
exports.item_list = async (req, res, next) => {
try{
let all_items = await Item.find({collectionItem: req.params.id});
res.render('item_list.jade', { title: "NFT Marketplace", collection_list: all_items });
} catch (error) {
res.status(400).json(error: error);
}
}

Implement follow/unfollow like action using Node + Express.js and MongoDB

I'm trying to design a Twitter style API. I've come up with the following Schema:
User collection
username
password
Followers collection
follower_id
followee_id
start_date
end_date
Eg:
{
follower_id: 1235
followee_id: 1024
start_date: November 3, 2018
}
which means user 1024 follows user 1235.
Absence of end_date indicates that the user is still following.
Tweets collection
text
date_created
user_id
user_id will be a reference to User collection.
How can I achieve follow/unfollow action with above schema?
For the user collection, you could have a following[] array. As in, you put each user id that they are following in the array and then remove it when they unfollow a person. Then with express, you can handle a post request sent by the front end with express. Of course you would need a boolean to say like:
// To show the button saying "follow" or "unfollow"
var followingArray = [123414, 312456, 656465];
var following = false;
if (followingArray.indexOf(userid)) {
following = true;
} else {
following = false;
}
Obviously, this is just a concept and you would need other code from mongoose or the regular mongodb module to get the user id. This would also decide whether or not the post request link is going to be http://myapp.com/follow or http://myapp.com/unfollow. Based on these two post requests, you would handle the data like the user id with body-parser and express:
var bodyParser = require('body-parser');
const express = require('express');
const app = express();
const port = 8080
app.use( bodyParser.json() ); // to support JSON-encoded bodies
app.use(bodyParser.urlencoded({ // to support URL-encoded bodies
extended: true
}));
app.post('/follow', (req, res) => {
//handle data here for a request to follow someone
var userid = req.body.userid;
})
app.post('/unfollow', (req, res) => {
//handle data here for a request to unfollow someone
var userid = req.body.userid
})
app.listen(port, () => {
console.log(`Server running on ${port}`);
})
Then you would use this data with mongoose or the mongodb module to remove or add that user id from the array and change the following boolean to true or false. Then if you wanted to add other features like a followers list or a following list, you could use the user id to do so. I am not giving exact code on how to do this as there are many concepts involved. I am just trying to give you the concepts that you would use in doing so.
I recommend this:
Keep the database as "flat" as possible. That means fewer collections.
You'd probably need only 2 collections: users and tweets
In your users schema should look something like this:
// Asuming you use mongoose, if you haven't heard about it, you should look it up.
const userSchema = new Schema({
username: {type: String, required: true, trim: true},
password: {type: String, required: true},
following: {type: Array}, // Store all user id of the people that this user is following in an array.
followers: {type: Array}, // Store all users id of this user's followers in an array.
tweets: {
type: Schema.Types.ObjectId, // Store all tweets by this user in an object.
ref: "Tweet" // Link to Tweet Schema
}
}, {_id: true});
Your tweets schema:
const tweetSchema = new Schema({
text: {type: String, required: true}
// You can also have a userId to store the id of the user whose this tweet belongs to
}, {_id: true});
You then can use create to create users or tweets, update to edit following or followers.
Obviously there are several ways to achieve what you want. This is just one of them.
Following Solution Works for me:
const mongoose = require("mongoose");
const userSchema = new mongoose.Schema(
{
name: {
type: String,
trim: true,
},
following: {
type: Array
},
followers: {
type: Array
},
},
{
timestamps: true,
}
);
const User = mongoose.model("User", userSchema);
module.exports = User;
router.patch("/api/v1/follow/", auth, async (req, res) => {
try {
let whomFollowed = await User.findByIdAndUpdate({ _id: req.body.followingId},
{ $push: { following: req.body.followerId } }
);
let whoFollowedMe = await User.findByIdAndUpdate({ _id: req.body.followerId },
{ $push: { followers: req.body.followingId } }
)
return res.status(200).send({ message: "User Follow Success"});
} catch (e) {
return res.status(500).send({ message: "User Follow Failed", data: e.message });
}
});
router.patch("/api/v1/unfollow/", auth, async (req, res) => {
try {
let whomUnFollowed = await User.findByIdAndUpdate({ _id: req.body.followingId },
{ $pull: { following: req.body.followerId } }
);
let whoUnFollowedMe = await User.findByIdAndUpdate({ _id: req.body.followerId },
{ $pull: { followers: req.body.followingId } }
)
return res.status(200).send({ message: "User UnFollow Success"});
} catch (e) {
return res.status(500).send({ message: "User UnFollow Failed", data: e.message });
}
});

Accessing Schema.Types.ObjectId's values with Mongoose

Hi I'm learning MongoDB and how to use Mongoose,
I'm trying to build a simple Workout manager app, where workouts have a name (Eg: "Upper Body"), an email account (which is used to identify who created the workout) and array of exercise consisting of references to exercises.
I want to be able to have a query where I can access the exercise title(Eg: "Push Ups") by using just the workout name.
My workout.js Model:
var mongoose = require('mongoose');
const Schema = mongoose.Schema;
var Exercise = require('./exercise');
const workoutSchema = new Schema({
title: {type: 'String', required: true},
email: {type: 'String', required: true},
exercises: [{ type: Schema.Types.ObjectId, ref: 'Exercise' }],
});
module.exports = mongoose.model('Workout', workoutSchema);
My exercise.js Model:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var exerciseSchema = new mongoose.Schema({
title: {type: String, required: true}
}, {
timestamps: true
});
module.exports = mongoose.model('Exercise', exerciseSchema);
My Workout Controller looks like this :
var Workout = require('../models/workout');
var Exercises = require('../models/exercise');
exports.getWorkoutExercises = function(req, res) {
Workout.findOne({ title: req.params.workout_name})
.populate('exercises')
.exec((err, exercises) => {
if (err){
res.send(err);
}
/* I want to be able to return or access the exercises title */
res.json({exercises.title});
});
}
In My routes.js i have this :
workoutRoutes.get('/:workout_name', WorkoutController.getWorkoutExercises);
Any Help or tips is much appreciated !
I am not sure what you did here res.json({exercises.title}); Can you try this? Hope it works
Workout.findOne({ title: req.params.workout_name})
.populate('exercises')
.exec((err, workout) => {
if (err){
res.send(err);
} else {
res.send({ exercises: workout.exercises });
}
});
And then, on your client side, you can iterate the exercises array and access to the title of exercise

Resources