├── .gitignore ├── util ├── index.js ├── auth.js └── bcrypt.js ├── models ├── index.js ├── Post.js ├── Comment.js └── User.js ├── index.js ├── config.js ├── db └── index.js ├── README.md ├── app.js ├── middleware └── auth.js ├── package.json └── graphql ├── schema.js ├── queries.js ├── types.js └── mutations.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /util/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bcrypt: require("./bcrypt"), 3 | auth: require("./auth"), 4 | }; 5 | -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | User: require("./User"), 3 | Post: require("./Post"), 4 | Comment: require("./Comment"), 5 | }; 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const app = require("./app"); 2 | const { connectDB } = require("./db"); 3 | const { PORT } = require("./config"); 4 | 5 | connectDB(); 6 | app.listen(PORT); 7 | console.log("Server on port", PORT); 8 | -------------------------------------------------------------------------------- /util/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const { JWT_EXPIRES_IN, JWT_SECRET } = require("../config"); 3 | 4 | const createJWTToken = (user) => { 5 | return jwt.sign({ user }, JWT_SECRET, { 6 | expiresIn: JWT_EXPIRES_IN, 7 | }); 8 | }; 9 | 10 | module.exports = { 11 | createJWTToken, 12 | }; 13 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const { config } = require("dotenv"); 2 | config(); 3 | 4 | module.exports = { 5 | MONGODB_URI: process.env.MONGODB_URI || "mongodb://localhost/blogdb", 6 | PORT: process.env.PORT || 3000, 7 | JWT_SECRET: process.env.JWT_SECRET || "somesecretkey", 8 | JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || "1d", 9 | }; 10 | -------------------------------------------------------------------------------- /db/index.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const { MONGODB_URI } = require("../config"); 3 | 4 | const connectDB = async () => { 5 | try { 6 | await mongoose.connect(MONGODB_URI); 7 | console.log("Mongodb connected"); 8 | } catch (error) { 9 | console.error(error); 10 | } 11 | }; 12 | 13 | module.exports = { connectDB }; 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Graphql Nodejs Mongodb Blog API 2 | 3 | Simple GraphQL Blog API using Nodejs and Mongodb 4 | 5 | ### Requerimentos 6 | 7 | - Mongodb 8 | - Nodejs 9 | 10 | ### Environment variables 11 | 12 | ``` 13 | MONGODB_URI 14 | PORT 15 | JWT_SECRET 16 | JWT_EXPIRES_IN 17 | ``` 18 | 19 | ### Installation 20 | 21 | ``` 22 | npm i 23 | npm start 24 | ``` 25 | -------------------------------------------------------------------------------- /util/bcrypt.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require("bcryptjs"); 2 | 3 | const encryptPassword = async (password) => { 4 | const salt = await bcrypt.genSalt(10); 5 | return bcrypt.hash(password, salt); 6 | }; 7 | 8 | const comparePassword = async (password, hash) => { 9 | return await bcrypt.compare(password, hash); 10 | }; 11 | 12 | module.exports = { 13 | encryptPassword, 14 | comparePassword, 15 | }; 16 | -------------------------------------------------------------------------------- /models/Post.js: -------------------------------------------------------------------------------- 1 | const { Schema, model } = require("mongoose"); 2 | 3 | const postSchema = new Schema( 4 | { 5 | authorId: { 6 | type: String, 7 | required: true, 8 | }, 9 | title: { 10 | type: String, 11 | required: true, 12 | }, 13 | body: { 14 | type: String, 15 | required: true, 16 | }, 17 | }, 18 | { 19 | timestamps: true, 20 | } 21 | ); 22 | 23 | module.exports = model("Post", postSchema); 24 | -------------------------------------------------------------------------------- /models/Comment.js: -------------------------------------------------------------------------------- 1 | const { Schema, model } = require("mongoose"); 2 | 3 | const commentSchema = new Schema( 4 | { 5 | comment: { 6 | type: String, 7 | required: true, 8 | }, 9 | userId: { 10 | type: String, 11 | required: true, 12 | }, 13 | postId: { 14 | type: String, 15 | required: true, 16 | }, 17 | }, 18 | { 19 | timestamps: true, 20 | } 21 | ); 22 | 23 | module.exports = model("Comment", commentSchema); 24 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { graphqlHTTP } = require("express-graphql"); 3 | const schema = require("./graphql/schema"); 4 | const { authenticate } = require("./middleware/auth"); 5 | 6 | const app = express(); 7 | 8 | app.use(authenticate); 9 | 10 | app.get("/", (req, res) => res.json({ msg: "Welcome. Go to /graphql" })); 11 | 12 | app.use( 13 | "/graphql", 14 | graphqlHTTP({ 15 | schema, 16 | graphiql: true, 17 | }) 18 | ); 19 | 20 | module.exports = app; 21 | -------------------------------------------------------------------------------- /middleware/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const { JWT_SECRET } = require("../config"); 3 | 4 | const authenticate = (req, res, next) => { 5 | const token = req.headers.authorization?.split(" ")[1] || ""; 6 | try { 7 | const verified = jwt.verify(token, JWT_SECRET); 8 | req.verifiedUser = verified.user; 9 | next(); 10 | } catch (error) { 11 | // console.error("error:", error); 12 | next(); 13 | } 14 | }; 15 | 16 | module.exports = { 17 | authenticate, 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-javascript-blog", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon server.js", 8 | "start": "node server.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcryptjs": "^2.4.3", 15 | "dotenv": "^10.0.0", 16 | "express": "^4.17.2", 17 | "express-graphql": "^0.12.0", 18 | "graphql": "^16.2.0", 19 | "jsonwebtoken": "^8.5.1", 20 | "mongoose": "^6.1.5" 21 | }, 22 | "devDependencies": { 23 | "nodemon": "^2.0.15" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const { Schema, model } = require("mongoose"); 2 | 3 | const userSchema = new Schema( 4 | { 5 | username: { 6 | type: String, 7 | required: true, 8 | }, 9 | password: { 10 | type: String, 11 | required: true, 12 | select: false, 13 | }, 14 | email: { 15 | type: String, 16 | required: true, 17 | unique: true, 18 | match: [ 19 | /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, 20 | "Provide a valid email", 21 | ], 22 | }, 23 | displayName: { 24 | type: String, 25 | required: true, 26 | }, 27 | }, 28 | { 29 | timestamps: true, 30 | versionKey: false, 31 | } 32 | ); 33 | 34 | module.exports = model("User", userSchema); 35 | -------------------------------------------------------------------------------- /graphql/schema.js: -------------------------------------------------------------------------------- 1 | const { GraphQLSchema, GraphQLObjectType } = require("graphql"); 2 | 3 | // Queries 4 | const { users, user, posts, post, comments, comment } = require("./queries"); 5 | 6 | // Mutations 7 | const { 8 | register, 9 | login, 10 | createPost, 11 | addComment, 12 | updatePost, 13 | deletePost, 14 | updateComment, 15 | deleteComment, 16 | } = require("./mutations"); 17 | 18 | // Define QueryType 19 | const QueryType = new GraphQLObjectType({ 20 | name: "QueryType", 21 | description: "Queries", 22 | fields: { 23 | users, 24 | user, 25 | posts, 26 | post, 27 | comments, 28 | comment, 29 | }, 30 | }); 31 | 32 | // Define MutationType 33 | const MutationType = new GraphQLObjectType({ 34 | name: "MutationType", 35 | description: "Mutations", 36 | fields: { 37 | register, 38 | login, 39 | createPost, 40 | addComment, 41 | updatePost, 42 | deletePost, 43 | updateComment, 44 | deleteComment, 45 | }, 46 | }); 47 | 48 | module.exports = new GraphQLSchema({ 49 | query: QueryType, 50 | mutation: MutationType, 51 | }); 52 | -------------------------------------------------------------------------------- /graphql/queries.js: -------------------------------------------------------------------------------- 1 | const { GraphQLList, GraphQLID, GraphQLNonNull } = require("graphql"); 2 | const { UserType, PostType, CommentType } = require("./types"); 3 | const { User, Post, Comment } = require("../models"); 4 | 5 | const users = { 6 | type: new GraphQLList(UserType), 7 | description: "Retrieves a list of users", 8 | resolve: () => User.find(), 9 | }; 10 | 11 | const user = { 12 | type: UserType, 13 | description: "retrieves a single user", 14 | args: { 15 | id: { type: new GraphQLNonNull(GraphQLID) }, 16 | }, 17 | resolve: (_, { id }) => User.findById(id), 18 | }; 19 | 20 | const posts = { 21 | type: new GraphQLList(PostType), 22 | description: "retrieves a list of posts", 23 | resolve: () => Post.find(), 24 | }; 25 | 26 | const post = { 27 | type: PostType, 28 | description: "retrieves a single post", 29 | args: { id: { type: GraphQLID } }, 30 | resolve: (_, { id }) => Post.findById(id), 31 | }; 32 | 33 | const comments = { 34 | type: new GraphQLList(CommentType), 35 | description: "Retrieves list of commnets", 36 | resolve: () => Comment.find(), 37 | }; 38 | 39 | const comment = { 40 | type: CommentType, 41 | description: "Retrieves a single comment", 42 | args: { 43 | id: { type: new GraphQLNonNull(GraphQLID) }, 44 | }, 45 | resolve: (_, { id }) => Comment.findById(id), 46 | }; 47 | 48 | module.exports = { users, user, posts, post, comments, comment }; 49 | -------------------------------------------------------------------------------- /graphql/types.js: -------------------------------------------------------------------------------- 1 | const { 2 | GraphQLObjectType, 3 | GraphQLID, 4 | GraphQLString, 5 | GraphQLList, 6 | } = require("graphql"); 7 | const { Post, Comment, User } = require("../models"); 8 | 9 | const UserType = new GraphQLObjectType({ 10 | name: "User", 11 | description: "User type", 12 | fields: () => ({ 13 | id: { type: GraphQLID }, 14 | username: { type: GraphQLString }, 15 | email: { type: GraphQLString }, 16 | displayName: { type: GraphQLString }, 17 | }), 18 | }); 19 | 20 | const PostType = new GraphQLObjectType({ 21 | name: "Post", 22 | description: "Post Type", 23 | fields: () => ({ 24 | id: { type: GraphQLID }, 25 | title: { type: GraphQLString }, 26 | body: { type: GraphQLString }, 27 | author: { 28 | type: UserType, 29 | resolve(parent) { 30 | return User.findById(parent.authorId); 31 | }, 32 | }, 33 | comments: { 34 | type: new GraphQLList(CommentType), 35 | resolve(parent) { 36 | return Comment.find({ postId: parent.id }); 37 | }, 38 | }, 39 | }), 40 | }); 41 | 42 | const CommentType = new GraphQLObjectType({ 43 | name: "Comment", 44 | description: "comments type", 45 | fields: () => ({ 46 | id: { type: GraphQLID }, 47 | comment: { type: GraphQLString }, 48 | user: { 49 | type: UserType, 50 | resolve(parent) { 51 | return User.findById(parent.userId); 52 | }, 53 | }, 54 | post: { 55 | type: PostType, 56 | resolve(parent) { 57 | return Post.findById(parent.postId); 58 | }, 59 | }, 60 | }), 61 | }); 62 | 63 | module.exports = { 64 | UserType, 65 | PostType, 66 | CommentType, 67 | }; 68 | -------------------------------------------------------------------------------- /graphql/mutations.js: -------------------------------------------------------------------------------- 1 | const { GraphQLString, GraphQLID, GraphQLNonNull } = require("graphql"); 2 | const { User, Post, Comment } = require("../models"); 3 | const { auth, bcrypt } = require("../util"); 4 | const { PostType, CommentType } = require("./types"); 5 | 6 | const register = { 7 | type: GraphQLString, 8 | args: { 9 | username: { type: new GraphQLNonNull(GraphQLString) }, 10 | email: { type: new GraphQLNonNull(GraphQLString) }, 11 | password: { type: new GraphQLNonNull(GraphQLString) }, 12 | displayName: { type: new GraphQLNonNull(GraphQLString) }, 13 | }, 14 | async resolve(_, { username, email, password, displayName }) { 15 | const user = new User({ username, email, password, displayName }); 16 | user.password = await bcrypt.encryptPassword(user.password); 17 | await user.save(); 18 | 19 | const token = auth.createJWTToken({ 20 | _id: user._id, 21 | email: user.email, 22 | displayName: user.displayName, 23 | }); 24 | return token; 25 | }, 26 | }; 27 | 28 | const login = { 29 | type: GraphQLString, 30 | args: { 31 | email: { type: new GraphQLNonNull(GraphQLString) }, 32 | password: { type: new GraphQLNonNull(GraphQLString) }, 33 | }, 34 | async resolve(_, { email, password }) { 35 | const user = await User.findOne({ email }).select("+password"); 36 | 37 | if (!user) throw new Error("Invalid Username"); 38 | 39 | const validPassword = await bcrypt.comparePassword(password, user.password); 40 | 41 | if (!validPassword) throw new Error("Invalid Password"); 42 | 43 | const token = auth.createJWTToken({ 44 | _id: user._id, 45 | email: user.email, 46 | displayName: user.displayName, 47 | }); 48 | 49 | return token; 50 | }, 51 | }; 52 | 53 | const createPost = { 54 | type: PostType, 55 | description: "create a new blog post", 56 | args: { 57 | title: { type: new GraphQLNonNull(GraphQLString) }, 58 | body: { type: new GraphQLNonNull(GraphQLString) }, 59 | }, 60 | async resolve(_, args, { verifiedUser }) { 61 | if (!verifiedUser) throw new Error("You must be logged in to do that"); 62 | 63 | const userFound = await User.findById(verifiedUser._id); 64 | if (!userFound) throw new Error("Unauthorized"); 65 | 66 | const post = new Post({ 67 | authorId: verifiedUser._id, 68 | title: args.title, 69 | body: args.body, 70 | }); 71 | 72 | return post.save(); 73 | }, 74 | }; 75 | 76 | const updatePost = { 77 | type: PostType, 78 | description: "update a blog post", 79 | args: { 80 | id: { type: new GraphQLNonNull(GraphQLID) }, 81 | title: { type: new GraphQLNonNull(GraphQLString) }, 82 | body: { type: new GraphQLNonNull(GraphQLString) }, 83 | }, 84 | async resolve(_, { id, title, body }, { verifiedUser }) { 85 | if (!verifiedUser) throw new Error("Unauthorized"); 86 | 87 | const postUpdated = await Post.findOneAndUpdate( 88 | { _id: id, authorId: verifiedUser._id }, 89 | { title, body }, 90 | { 91 | new: true, 92 | runValidators: true, 93 | } 94 | ); 95 | 96 | if (!postUpdated) throw new Error("No post for given id"); 97 | 98 | return postUpdated; 99 | }, 100 | }; 101 | 102 | const deletePost = { 103 | type: GraphQLString, 104 | description: "Delete post", 105 | args: { 106 | postId: { type: new GraphQLNonNull(GraphQLID) }, 107 | }, 108 | async resolve(_, args, { verifiedUser }) { 109 | const postDeleted = await Post.findOneAndDelete({ 110 | _id: args.postId, 111 | authorId: verifiedUser._id, 112 | }); 113 | if (!postDeleted) 114 | throw new Error("No post with given ID Found for the author"); 115 | 116 | return "Post deleted"; 117 | }, 118 | }; 119 | 120 | const addComment = { 121 | type: CommentType, 122 | description: "Create a new comment for a blog post", 123 | args: { 124 | comment: { type: new GraphQLNonNull(GraphQLString) }, 125 | postId: { type: new GraphQLNonNull(GraphQLID) }, 126 | }, 127 | resolve(_, { postId, comment }, { verifiedUser }) { 128 | const newComment = new Comment({ 129 | userId: verifiedUser._id, 130 | postId, 131 | comment, 132 | }); 133 | return newComment.save(); 134 | }, 135 | }; 136 | 137 | const updateComment = { 138 | type: CommentType, 139 | description: "update a comment", 140 | args: { 141 | id: { type: new GraphQLNonNull(GraphQLID) }, 142 | comment: { type: new GraphQLNonNull(GraphQLString) }, 143 | }, 144 | async resolve(_, { id, comment }, { verifiedUser }) { 145 | if (!verifiedUser) throw new Error("UnAuthorized"); 146 | 147 | const commentUpdated = await Comment.findOneAndUpdate( 148 | { 149 | _id: id, 150 | userId: verifiedUser._id, 151 | }, 152 | { 153 | comment, 154 | }, 155 | { 156 | new: true, 157 | runValidators: true, 158 | } 159 | ); 160 | 161 | if (!commentUpdated) throw new Error("No comment with the given ID"); 162 | 163 | return commentUpdated; 164 | }, 165 | }; 166 | 167 | const deleteComment = { 168 | type: GraphQLString, 169 | description: "delete a comment", 170 | args: { 171 | id: { type: new GraphQLNonNull(GraphQLID) }, 172 | }, 173 | async resolve(_, { id }, { verifiedUser }) { 174 | if (!verifiedUser) throw new Error("Unauthorized"); 175 | 176 | const commentDelete = await Comment.findOneAndDelete({ 177 | _id: id, 178 | userId: verifiedUser._id, 179 | }); 180 | 181 | if (!commentDelete) 182 | throw new Error("No comment with the given ID for the user"); 183 | 184 | return "Comment deleted"; 185 | }, 186 | }; 187 | 188 | module.exports = { 189 | register, 190 | login, 191 | createPost, 192 | addComment, 193 | updatePost, 194 | deletePost, 195 | updateComment, 196 | deleteComment, 197 | }; 198 | --------------------------------------------------------------------------------