I follow modular or component based strucutre. I found a sample repo.
https://github.com/sujeet-agrahari/node-express-clean-architecture
So, there is a main component.module.js files which is responsible for connecting all other pieces like controller, route, and services.
For controller, services are being injected using higher order functions. Now, controller are super easy to test, I can stub or mock services easily.
auth.module.js
const router = require('express').Router();
const {
makeExpressCallback,
makeValidatorCallback,
} = require('../../middlewares');
// validator
const AuthValidator = require('./auth.validator');
// service
const { doRegister, doLogin, doCheckUserExist } = require('./auth.service');
const { BadRequestError } = require('../../utils/api-errors');
// controller
const controller = require('./auth.controller');
const register = controller.register({ BadRequestError, doCheckUserExist, doRegister });
const login = controller.login({ doCheckUserExist, doLogin });
const AuthController = { register, login };
// routes
const routes = require('./auth.routes')({
router,
AuthController,
AuthValidator,
makeValidatorCallback,
makeExpressCallback,
});
module.exports = {
AuthController,
AuthService: {
doCheckUserExist,
doLogin,
doRegister,
},
AuthRoutes: routes,
};
auth.controller.js
const login = (doCheckUserExist, doLogin) => async (httpRequest) => {
const { username, password } = httpRequest.body;
const userData = await doCheckUserExist({ username });
const loginData = {
username,
role: userData.role_id,
passedPassword: password,
actualPassword: userData.password,
};
const loginResult = await doLogin(loginData);
return {
statusCode: 200,
body: {
success: true,
message: 'Successfully logged in!',
data: loginResult,
},
};
};
const register = ({ BadRequestError, doCheckUserExist, doRegister }) => async (httpRequest) => {
const { username, password } = httpRequest.body;
try {
await doCheckUserExist({ username });
} catch (error) {
// user doesn't exist
const registerResult = await doRegister({ username, password });
return {
statusCode: 200,
body: {
success: true,
message: 'Registered successfully!',
data: registerResult,
},
};
}
throw new BadRequestError('User already exist!');
};
module.exports = { register, login };
Things are fine with the controller, now the problem is with the services. I can't find any pattern to make them thin and clean.
auth.services.js
const {
JWT_ACCESS_TOKEN_SECRET,
ACCESS_TOKEN_EXPIRES_IN,
SIGN_OPTION,
} = require('config');
const bcrypt = require('bcryptjs');
const { User } = require('../../db');
const { generateJWT } = require('./jwt.service');
const { NotFoundError, BadRequestError } = require('../../utils/api-errors');
const doRegister = async ({ username, password }) => {
const user = await User.create({
username,
password,
role_id: 1, // assign role id here
});
// generate access token
const payload = {
username,
role: user.role_id,
};
const token = await generateJWT({
secretKey: JWT_ACCESS_TOKEN_SECRET,
payload,
signOption: {
...SIGN_OPTION,
expiresIn: ACCESS_TOKEN_EXPIRES_IN,
},
});
return {
access_token: token,
...payload,
};
};
const doLogin = async ({
username, userRole, passedPassword, actualPassword,
}) => {
const isValidPass = bcrypt.compareSync(passedPassword, actualPassword);
if (!isValidPass) throw new BadRequestError('Username or Password is invalid!');
// generate access token
const payload = {
username,
role: userRole,
};
const token = await generateJWT({
secretKey: JWT_ACCESS_TOKEN_SECRET,
payload,
signOption: {
...SIGN_OPTION,
expiresIn: ACCESS_TOKEN_EXPIRES_IN,
},
});
return {
access_token: token,
...payload,
};
};
const doCheckUserExist = async ({ username }) => {
const user = await User.findOne({
where: {
username,
},
});
if (!user) throw new NotFoundError('User not found!');
return user;
};
module.exports = { doRegister, doLogin, doCheckUserExist };
A lot is happening in the services, model imports, constants imports, and other utils.
Now services become really hard to test.
Is there any way or pattern I can separate some logic from services and make them lighter?
I can implement reository pattern for db methods, but I am not aware how I can implement using sequelize?
Should I use also higher order function to inject all the utils and constants in the service like I did for controller?
Related
I keep trying to call an api from my getServerSideProps function in my Next.js application, but everytime I try to do so I get this error
"Error: Error serializing .customerId returned from getServerSideProps in "/pricing".
Reason: undefined cannot be serialized as JSON. Please use null or omit this value."
This is the code for the Pricing page
const Pricing = ({customerId}) => {
console.log(customerId, 'Customerid Pricing')
return(
<div>
<ProductDisplay customerId={customerId}/>
</div>
)
}
export const getServerSideProps = async (ctx) => {
//create customer variable
let customerId
// Create authenticated Supabase Client
const supabase = createServerSupabaseClient(ctx)
// get the user session from supabase
const { data: { session }, } = await supabase.auth.getSession()
//get the user email from the session
const email = session.user.email
//create stripe customer
const createCustomer = async () => {
let createdCustomer;
let customerAvailable;
try {
const checkCustomer = await stripe.customers.search({
query: `email: '${email}'`
})
createdCustomer = JSON.parse(JSON.stringify(checkCustomer))
customerAvailable = createdCustomer.data
} catch (error) {
console.log(error)
}
if (customerAvailable.length) {
customerId = createdCustomer.data[0].id
} else {
try {
const res = await fetch('/api/create-customer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email
})
})
const newCustomer = await res.json();
customerId = newCustomer.customer.id
} catch (error) {
console.log(error)
}
}
}
if (session) {
createCustomer()
} else {
return{
redirect: {
destination: '/signin',
permanent: false,
}
}
}
return {
props: {
customerId
},
}
}
export default Pricing;
This is the /api/create-customer the fetch function is trying to call
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
const handler = async (req, res) => {
const {email} = req.body
try {
const customer = await stripe.customers.create({
email: email
});
res.status(200).json({
code: 'customer_created',
customer,
})
} catch (error) {
console.log(error);
res.status(400).json({
code: 'customer_creation_failed',
error: error
})
}
};
export default handler;
please help me, thank you very much.
I created an API for following and followers user for Social Media Application, while request from postman getting empty object: {} But it seems to me to be correct.
Model:
const mongoose = require("mongoose");
const UserSchema = mongoose.Schema({
username: {
type: String,
required: true,
},
password: {
type: String,
required: true,
},
firstname: {
type: String,
required: true,
},
lastname: {
type: String,
required: true,
},
isAdmin: {
type: Boolean,
default: false,
},
profilePicture: String,
coverPicture: String,
about: String,
livesin: String,
workAt: String,
relationship: String,
followers: [],
following: []
},{
timestamps:true
}
);
const UserModel = mongoose.model("Users", UserSchema);
module.exports = UserModel;
UserControler:
const UserModel = require("../Models/Usermodel");
const bcrypt = require("bcryptjs");
const followUser = async (req, res) => {
const id = req.params.id.trim();
const { currentUserId } = req.body;
if (currentUserId === id) {
res.status(403).send("Action forbiden");
} else {
try {
const followUser = await UserModel.findById(id);
const followingUser = await UserModel.findById(currentUserId);
if (!followUser.followers.includes(currentUserId)) {
await followUser.updateOne({ $push: { followers: currentUserId } });
await followingUser.updateOne({ $push: { following: id } });
res.status(200).send({message:"User Followed"});
} else {
res.status(403).send("User alredy followed by you!");
}
} catch (error) {
res.status(500).send(error);
}
}
};
module.exports = { getUser, updateUser, userDelete, followUser };
UserRoute:
const express = require("express");
const {getUser,updateUser, userDelete, followUser} = require("../Controller/userControler");
const router = express.Router()
router.get("/:id",getUser)
router.put("/:id",updateUser)
router.delete("/:id", userDelete)
router.put("/:id/follow", followUser)
module.exports=router;
index.js:
app.use("/user",UserRoute)
Here is the complete details regarding the error, let me know what happens in the code, thank you.
i assume that you have all the other functions other than followUser in your controller.js
The thing is that you must first specify the field name on the basis of which you want to update the document.
Here is what you need to do;
const UserModel = require("../Models/Usermodel");
const bcrypt = require("bcryptjs");
const mongoose = require("mongoose");//updated line
const followUser = async (req, res) => {
const id = req.params.id.trim();
const { currentUserId } = req.body;
if (currentUserId === id) {
res.status(403).send("Action forbiden");
} else {
try {
const followUser = await UserModel.findById({_id: mongoose.Types.ObjectId(id)});
const followingUser = await UserModel.findById({_id: mongoose.Types.ObjectId(currentUserId)});
if (!followUser.followers.includes(currentUserId)) {
await followUser.updateOne({_id: mongoose.Types.ObjectId(*id of the user you want to update*)},{ $push: { followers: currentUserId } });
await followingUser.updateOne({_id: mongoose.Types.ObjectId(*id of the user you want to update*)}{ $push: { following: id } });
res.status(200).send({message:"User Followed"});
} else {
res.status(403).send("User alredy followed by you!");
}
} catch (error) {
res.status(500).send(error);
}
}
};
module.exports = { getUser, updateUser, userDelete, followUser };
And while hitting the api pls make sure that your route should be
localhost:port-number/user/12345789/follow
and also make sure that the API type in postman must be same as in the backend e.g; PUT
please try findByIdAndUpdate query insted of using updateOne
Hello I am using mocha, chai, sinon and supertest.
I have a logging route I cannot stub/spy/mock (tried all 3) an inner class method authService.findByEmail(req.body.email) inside the loginRoute function
this is the function I want to test
login.js
const jwtGenerator = (payload) => jwt.sign(payload, process.env.TOKEN, { expiresIn: "1h" });
const loginRoute = async (req, res, next) => {
try {
req.body.password = String(req.body.password);
// db query trying to force a sinon.stub to resolve a fake value. But code wont pass here hence 500 error
const userQuery = await authService.findByEmail(req.body.email);
const compare = await bcrypt.compare(req.body.password, userQuery.password);
if (!compare) {
throw createError(401, 'Incorrect password.');
}
const user = {
id: userQuery.id, role: userQuery.is_admin ? "Administrator" : "User", email: userQuery.email, Authorized: true
}
const token = jwtGenerator(user);
return res
.cookie("access_token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
}).status(200).json({ message: "Logged in successfully š š", user, token });
} catch (error) {
next(error);
}
}
router.post("/auth/login", loginRoute);
Here is my test file
login.spec.js
const chai = require("chai");
const sinonChai = require("sinon-chai");
chai.use(sinonChai);
const supertest = require("supertest");
const sinon = require("sinon");
const AuthService = require("../../services/authService"); // the class
describe("/login", () => {
const createAccessToken = (payload) => jwt.sign(payload, TOKEN, {expiresIn: "1h"});
const authService = new AuthService();
let userQuery, token, response, cookie;
const agent = supertest(app);
let loginDetail = {
email: "admin#test.com",
password: "123456"
};
let adminUser = {
id: 1,
email: "admin#test.com",
password: "123456",
is_admin: true
}
const user = {
id: 1,
email: "admin#test.com",
password: "123456",
is_admin: true
}
it("login controller function", async () => {
token = createAccessToken(user);
// I have commented out the set as they aren't doing anything atm
response = await agent.post("/auth/login").send(loginDetail)//.set("Accept", "application/json")
// .set("Cookie", `access-token=${token}`);
userQuery = sinon.stub(authService, "findByEmail").withArgs(loginDetail.email).resolves(adminUser);
// authService.findByEmail(loginDetail.email); // even if I call it the calledWith might work but response.status is still 500
// userQuery.resolves(adminUser);
expect(userQuery).to.have.been.calledWith(loginDetail.email); // is never called
// expect(userQuery).to.have.been.called // never called
expect(response.status).to.deep.equal(200); // returns 500
});
});
I must be doing something wrong somewhere I have tried everything I dont know how to have all the inner methods/functions inside the loginRoute to be called automatically as I call the loginRoute through supertest(app).post("/auth/login")
Iām refactoring a Google Books app from a Restful API to GraphQL, and I am stuck on a mutation not behaving the way I expect.
When a user fills out the form found on Signup.js the Mutation ADD_USER should create a user within Mongoose, this user should have a JWT token assigned to them, and user should be logged in upon successful execution of the Mutation.
Actions observed:
ā¢ Mutation is being fired off from the front end. When I open developer tools in the browser I can see the Username, Email and Password being passed as variables.
ā¢ I have tried console logging the token, and keep getting an undefined return
ā¢ When I try to run the mutation in the GraphQL sandbox I get a null value returned.
ā¢ When I console log the args in resolvers.js no value appears on the console, which tells me the request is not reaching the resolver.
SignupForm.js (React FE Page)
import React, { useState } from "react";
import { Form, Button, Alert } from "react-bootstrap";
import { useMutation } from "#apollo/client";
import { ADD_USER } from "../utils/mutations";
import Auth from "../utils/auth";
const SignupForm = () => {
// set initial form state
const [userFormData, setUserFormData] = useState({
username: "",
email: "",
password: "",
});
// set state for form validation
const [validated] = useState(false);
// set state for alert
const [showAlert, setShowAlert] = useState(false);
const [addUser] = useMutation(ADD_USER);
const handleInputChange = (event) => {
const { name, value } = event.target;
setUserFormData({ ...userFormData, [name]: value });
};
const handleFormSubmit = async (event) => {
event.preventDefault();
// check if form has everything (as per react-bootstrap docs)
const form = event.currentTarget;
if (form.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
}
try {
///Add user is not returning data. payload is being passed as an object
const response = await addUser({
variables: { ...userFormData },
});
if (!response.ok) {
throw new Error("OH NO!SOMETHING WENT WRONG!");
}
const { token, user } = await response.json();
console.log(user);
Auth.login(token);
} catch (err) {
console.error(err);
setShowAlert(true);
}
setUserFormData({
username: "",
email: "",
password: "",
});
};
Mutation.js
export const ADD_USER = gql`
mutation addUser($username: String!, $email: String!, $password: String!) {
addUser(username: $username, email: $email, password: $password) {
token
user {
username
email
}
}
}
`;
typeDefs.js
const { gql } = require("apollo-server-express");
const typeDefs = gql`
input SavedBooks {
authors: [String]
description: String
bookId: String
image: String
link: String
title: String
}
type Books {
authors: [String]
description: String
bookId: ID
image: String
link: String
title: String
}
type User {
_id: ID
username: String
email: String
password: String
savedBooks: [Books]
}
type Auth {
token: ID!
user: User
}
type Query {
me: User
}
type Mutation {
##creates a user profile through the Auth type, that way we can pass a token upon creation
addUser(username: String!, email: String!, password: String!): Auth
login(email: String!, password: String!): Auth
saveBook(bookData: SavedBooks): User
deleteBook(bookId: ID!): User
}
`;
module.exports = typeDefs;
ā
resolvers.js
const { User, Book } = require("../models");
const { AuthenticationError } = require("apollo-server-express");
const { signToken } = require("../utils/auth");
const resolvers = {
Query: {
me: async (parent, args, context) => {
if (context.user) {
return User.findOne({ _id: context.user._id }).populate("books");
}
throw new AuthenticationError("You need to log in");
},
},
};
Mutation: {
//try refactoring as a .then
addUser: async (parent, args) => {
//create user profile
await console.log("resolver test");
console.log(args);
const user = await User.create({ username, email, password });
//assign token to user
const token = signToken(user);
return { token, user };
};
login: async (parent, { email, password }) => {
const user = User.findOne({ email });
if (!user) {
throw new AuthenticationError("Invalid Login Credentials");
}
const correctPw = await profile.isCorrectPassword(password);
if (!correctPw) {
throw new AuthenticationError("Invalid Login Credentials");
}
const token = signToken(user);
return { token, user };
};
saveBook: async (parent, { bookData }, context) => {
if (context.user) {
return User.findOneAndUpdate(
{ _id: context.user._id },
{ $addToSet: { savedBooks: bookData } },
{ new: true }
);
}
throw new AuthenticationError("You need to log in");
};
deleteBook: async (parent, { bookId }, context) => {
if (context.user) {
return User.findOneAndUpdate(
{ _id: contex.user._id },
//remove selected books from the savedBooks Array
{ $pull: { savedBooks: context.bookId } },
{ new: true }
);
}
throw new AuthenticationError("You need to log in");
};
}
module.exports = resolvers;
auth.js
const jwt = require("jsonwebtoken");
// set token secret and expiration date
const secret = "mysecretsshhhhh";
const expiration = "2h";
module.exports = {
// function for our authenticated routes
authMiddleware: function ({ req }) {
// allows token to be sent via req.query or headers
let token = req.query.token || req.headers.authorization || req.body.token;
// ["Bearer", "<tokenvalue>"]
if (req.headers.authorization) {
token = token.split(" ").pop().trim();
}
if (!token) {
return req;
}
// verify token and get user data out of it
try {
const { data } = jwt.verify(token, secret, { maxAge: expiration });
req.user = data;
} catch {
console.log("Invalid token");
return res.status(400).json({ message: "invalid token!" });
}
// send to next endpoint
return req;
},
signToken: function ({ username, email, _id }) {
const payload = { username, email, _id };
return jwt.sign({ data: payload }, secret, { expiresIn: expiration });
},
};
Basically, I have combed from front to back end looking for where I introduced this bug, and am stuck. Any suggestions or feedback is greatly appreciated.
I was able to figure out the issue. First, a syntax error on resolver.js was preventing my mutations from being read.
Next, I made the following adjustment to handleFormSubmit on SignupForm.js
try {
///Add user is not returning data. payload is being passed as an object
const {data} = await addUser({
variables: { ...userFormData },
});
console.log(data)
console.log(userFormData)
**Auth.login(data.addUser.token);**
} catch (err) {
console.error(err);
setShowAlert(true);
}
That way my FE was properly accounting for what my Auth Middleware was passing back after successful user creation. Thanks for your help xadm, being able to talk this out got me thinking about where else to attack the bug.
I use Sequelize to work with the database.
In my project, I encountered code duplication, and decided to study the repository design pattern, which will separate the work with the database and data output from the business logic. Having studied the information on the Internet, I decided to consolidate the material and check with more experienced programmers. I use the repository pattern correctly?
controller/user.controller.js
const userService = require('../services/user.service');
exports.userCreate = async (req, res, next) => {
try {
const user = await userService.userCreate(req.body);
return res.status(201).json(user);
} catch (e) {
return next(e);
}
}
services/user.service.js
const ApiError = require('../utils/error');
const userRepo = require('../repositories/user.repository');
exports.userCreate = async (data) => {
if (!data) throw ApiError.badRequest('Bad');
const { login } = data;
if (!login) throw ApiError.badRequest('Bad');
const password = 'hash';
const user = await userRepo.userCreate({login, password});
return user;
}
repositories/user.repository.js
const User = require('../models/User');
exports.userCreate = async (data) => {
const { login, passowrd } = data;
const user = await User.create({login, password});
return {
login: user.login,
}
}
models/User.js
const { sequelize, Sequelize } = require('../config/db');
const User = sequelize.define('users', {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
allowNull: false,
primaryKey: true,
},
login: {
type: Sequelize.STRING,
allowNull: false,
},
password: {
type: Sequelize.STRING,
allowNull: false,
}
});
module.exports = User;
Your User model is fine, but the structure of the service seems a little weird. if all three of the functions userCreates are in separate files I'd recommend you combine them and rename services to Controllers, as services are usually used on the client side of things.
services/user.service.js
const User = require('../models/User')
// const userRepo = require('../repositories/user.repository') Not sure what this does
const ApiError = require('../utils/error')
module.exports = {
async createUser (req, res) {
try {
if (!req.body) throw ApiError.badRequest('Bad')
const { login } = req.body
if (!login) throw ApiError.badRequest('Bad')
const password = 'hash'
const user = await User.create({ login, password })
res.stauts(201).json(user.login)
} catch (err) {
console.log(err)
res.staus(500).send({
error: err
})
// return next(err)
}
}
}