I am using socket.io with express and typescript, when want to emit to particular logged in user it is not working, the rest is working fine, when new user join and other things, in App.ts in backend it looks like:
httpServer.listen(8000, () => {
console.log(`app is running on: http://localhost:8000`);
});
//SocketIO
const io = new Server(httpServer, {
cors: {
credentials: true,
},
});
app.set("socketio", io);
io.use((socket: SocketWithUser, next: any) => {
const token: any = socket.handshake.query.token;
if (token) {
try {
const payload = jwt.verify(
token,
<string>process.env.JWT_TOKEN
) as DataStoredInToken;
socket.userId = payload._id;
return next();
} catch (err) {
next(err);
}
} else {
return next(new Error("Access Denied"));
}
});
io.on("connection", (socket: SocketWithUser) => {
if (socket.userId) {
socket.join(socket.userId);
socket.emit("joined", `user ${socket.userId} joined`);
}
socket.on("disconnect", () => {
console.log("disconnect");
});
});
and in another route sockethandler
import { Request } from "express";
import { NotificationProps } from "types/notification";
export const sendNotification = (
req: Request,
notification: NotificationProps
) => {
const io = req.app.get("socketio");
io.sockets
.in(String(`${notification.receiver}`))
.emit("newNotification", notification);
};
the like post route looks like
export const likePost = async (req: RequestWithUser, res: Response) => {
const { postId } = req.body;
const post = await PostModel.findById(postId);
if (!post) return res.status(400).send({ msg: `post does not exist` });
const checkIfLiked = post.likes.find(
(item: any) => String(item.user._id) === String(req.user_id)
);
if (!checkIfLiked) {
await post.updateOne(
{
$push: { likes: { user: req.user_id } },
},
{ new: true }
);
const notification = new Notification({
sender: req.user_id,
receiver: post.user,
notificaitonType: "like",
});
await notification.save();
sendNotification(req, notification);
return res.status(200).send({ success: true });
}
const postWithOutLike = await post.updateOne(
{
$pull: { likes: { user: req.user_id } },
},
{ new: true }
);
return res.status(200).send({ postWithOutLike });
};
in the frontend react app just calling it like:
socketIo().on("newNotification", (data) => {
console.log({ data });
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
any help please?
I believe you need want io.in
// to all clients in room
io.in(notification.receiver).emit('newNotification', notification);
Ref
Related
I have an Express based CRUD application which uses MongoDB as its DB. I have noticed that some concurrent writes fail if they use bulkWrite and session.
A simplified example looks like this:
import express from 'express';
import { v4 } from 'uuid';
import mongoose from 'mongoose';
const router = express.Router();
const mongoString = 'mongodb://127.0.0.1:27017/testMongo?directConnection=true';
const port = 3288;
const testId = mongoose.Types.ObjectId();
const Model = mongoose.model('Data', new mongoose.Schema({
whateverString: {
required: true,
type: String,
},
}));
mongoose.connect(mongoString);
const database = mongoose.connection;
database.on('error', (error) => {
console.log(error);
});
database.once('connected', async () => {
console.log('Database connected');
// Add test data if not exists
if (!await Model.exists({ _id: testId })) {
const data = new Model({
_id: testId,
whateverString: v4(),
});
await data.save();
}
});
const app = express();
app.use(express.json());
router.post('/', async (req, res) => {
const session = await mongoose.startSession();
session.startTransaction();
try {
await Model.bulkWrite([
{
updateOne: {
filter: {
_id: testId,
},
update: {
whateverString: v4(),
},
},
},
], { session });
await session.commitTransaction();
res.status(200).json({ allRight: true });
} catch (error) {
await session.abortTransaction();
console.log(error.message);
res.status(400).json({ message: error.message });
} finally {
session.endSession();
}
});
app.use('/', router);
app.listen(port, async () => {
console.log(`Server started at ${port}`);
});
What this does is:
connecting to Mongo
creating a test document
creating a web server and one post route
if the post route is called, the test document is updated with a random string in a bulkWrite and a session
Now take a simple client script which does three requests in parallel:
import fetch from 'node-fetch';
function doFetch() {
return fetch('http://localhost:3288', { method: 'post' });
}
async function myMain() {
try {
const promises = [doFetch(), doFetch(), doFetch()];
const response = await Promise.all(promises);
console.log('response', response.map(resp => ({ status: resp.status, text: resp.statusText })));
} catch (error) {
console.log(error);
}
}
myMain();
The result is: Only one DB query works, whereas the others fail with the error WriteConflict error: this operation conflicted with another operation. Please retry your operation or multi-document transaction.
I am rather new to MongoDB and Mongoose but in my understanding of databases, such a use-case should be fine. (In this example, the requests would overwrite each other and create chaos, but in the real-life use case that should not be a problem at all.)
Some observations I've made:
Without passing session to bulkWrite, everything works fine.
It is obviously some kind of race condition: sometimes two queries go through, sometimes only one.
Setting maxTransactionLockRequestTimeoutMillis to 20000 did not help.
If I include the fetching in the server process itself (after console.log('Server started ...), then everything works fine. I cannot explain that to be honest.
What am I doing wrong? How can I solve that problem?
Appendix: The package.json file of that example looks like this:
{
"name": "rest-api-express-mongo",
"dependencies": {
"express": "^4.17.3",
"mongoose": "^6.2.2",
"node-fetch": "^3.2.10",
"uuid": "^9.0.0"
},
"type": "module"
}
``
Thanks to the comment provided by Marco Luzzara I was able to refactor and solve the issue via callbacks.
The code being now:
let retry = 0;
await database.getClient().withSession(async (session) => {
try {
await session.withTransaction(async () => {
await Model.bulkWrite([
{
updateOne: {
filter: {
_id: testId,
},
update: {
whateverString: v4(),
},
},
},
], { session });
await session.commitTransaction();
res.status(200).json({ allRight: true });
});
} catch (error) {
console.log(error.message);
res.status(400).json({ message: error.message });
retry += 1;
if (retry > 5) {
session.endSession();
}
}
});
Just for reference - the whole file looks now like:
import express from 'express';
import { v4 } from 'uuid';
import mongoose from 'mongoose';
const router = express.Router();
const mongoString = 'mongodb://127.0.0.1:27017/testMongo?directConnection=true';
const port = 3288;
const testId = mongoose.Types.ObjectId();
const Model = mongoose.model('Data', new mongoose.Schema({
whateverString: {
required: true,
type: String,
},
}));
mongoose.connect(mongoString);
const database = mongoose.connection;
database.on('error', (error) => {
console.log(error);
});
database.once('connected', async () => {
console.log('Database connected');
// Add test data if not exists
if (!await Model.exists({ _id: testId })) {
const data = new Model({
_id: testId,
whateverString: v4(),
});
await data.save();
}
});
const app = express();
app.use(express.json());
router.post('/', async (req, res) => {
let retry = 0;
await database.getClient().withSession(async (session) => {
try {
await session.withTransaction(async () => {
await Model.bulkWrite([
{
updateOne: {
filter: {
_id: testId,
},
update: {
whateverString: v4(),
},
},
},
], { session });
await session.commitTransaction();
res.status(200).json({ allRight: true });
});
} catch (error) {
console.log(error.message);
res.status(400).json({ message: error.message });
retry += 1;
if (retry > 5) {
session.endSession();
}
}
});
});
app.use('/', router);
app.listen(port, async () => {
console.log(`Server started at ${port}`);
});
When I console.log the variable refToken before the get request the refToken shows what it contains but after that nothing, and my backend sends me the error 401 which means no token was provided!!
I am really confused here
utility file for expo-secure-store
import * as SecureStore from 'expo-secure-store';
const saveRefreshToken = async (value: string) => {
try {
await SecureStore.setItemAsync("refreshToken", value);
} catch (e) {
console.log("Cannot set refresh token")
}
}
const getRefreshToken = async () => {
try {
return await SecureStore.getItemAsync("refreshToken");
} catch (e) {
console.log("can't get requested refreshToken", e);
}
}
const deleteRefreshToken = async () => {
try {
await SecureStore.deleteItemAsync("refreshToken");
} catch (e) {
console.log("cannot delete refreshToken ", e);
}
}
export default {
saveRefreshToken,
getRefreshToken,
deleteRefreshToken
};
React native code
import axios from "../api/axios";
import useAuth from "./useAuth";
import storage from "../utility/storage";
import jwtDecode from "jwt-decode";
const useRefreshToken = () => {
const {setAuth, auth} = useAuth();
return async () => {
const refToken = await storage.getRefreshToken();
// withCredentials: true,
const response = await axios.get(`/auth/refresh/`, {
params: {refreshToken: refToken},
});
//#ts-ignore
setAuth({user: jwtDecode(response.data.accessToken), accessToken: response.data.accessToken});
return response.data.accessToken;
};
}
export default useRefreshToken;
Node.js backend part that deals with this request
// TODO: Generate new access token from refresh
export const refreshTokenSignIn = (
req: express.Request,
res: express.Response,
next: express.NextFunction
) => {
// const refreshToken = req.cookies.jwt;
const refreshToken = req.body.refreshToken;
try {
// Check if the refresh token is not null
if (!refreshToken)
return res.status(401).json({message: "No token provided!"});
// console.log(refreshToken);
const sql = `select * from token_records where refresh_token = '${refreshToken}'`;
// Check if there is such refresh token for the current user
db.query(
sql,
[refreshToken],
(err: QueryError, result: RowDataPacket[]) => {
if (err) return next(err);
if (result.length === 0)
return res.status(403).json({message: "You don't have access"});
//#ts-ignore
jwt.verify(
refreshToken,
//#ts-ignore
process.env.JWT_REFRESH_SECRET,
(err2: VerifyErrors, user: JwtPayload) => {
if (err2) return res.status(403).json({message: "You don't have access"});
const accessToken = jwt.sign(
{
user_id: user.user_id,
firstname: user.firstname,
lastname: user.lastname,
username: user.username,
email: user.email,
role: user.role,
},
//#ts-ignore
process.env.JWT_SECRET
);
res.status(200).json({accessToken: accessToken});
next();
}
);
}
);
} catch (e) {
console.log(e);
next();
}
};
I have the following controller for updating project which gets id from param.
const updateProject = async (req: Request, res: Response, next: NextFunction) => {
const { title, desc } = req.body;
const { projectID } = req.params;
const { user } = <any>req;
if (!isValidObjectId(projectID)) {
return next(new ErrorResponse("Invalid project ID", 400));
}
try {
const project = await Project.findOneAndUpdate(
{ _id: projectID, user: user._id },
{
title: title,
desc: desc,
},
{ new: true }
);
if (!project) {
return next(new ErrorResponse("Project not found", 404));
}
res.status(200).json(project);
} catch (err) {
next(err);
}
};
I've written a test for this controller but I keep getting "Bad Request". But when I test it manually using Postman, it works fine. And other tests that do not require params work fine too.
import mongoose from "mongoose";
const projectOneId = new mongoose.Types.ObjectId();
export const projectOne = {
_id: projectOneId,
title: "First Project",
desc: "This is the first project",
user: userOneId,
};
describe("/api/projects", () => {
//login using cookie before running the test
let cookie: string;
beforeEach(async () => {
await request(app)
.post("/api/auth/login")
.send(userOne)
.expect(200)
.then((res) => {
const cookies = res.headers["set-cookie"][0]
.split(",")
.map((item: string) => item.split(";")[0]);
cookie = cookies.join(";");
});
});
describe("PUT /api/projects/:projectID", () => {
it("Should update a project", async () => {
await request(app)
.put(`/api/projects/:projectID`)
.set("Cookie", cookie)
.query({ projectID: projectOne._id.toString() })
.send(projectOne)
.expect(200);
});
});
I am developing a backend project using nodejs, express and mongoose. But when I try to test my endpoints on Postman I keep getting this error. I am new to nodejs so I am not quite sure what this means.
This is my main js file index.js
const app = express();
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
require('dotenv/config');
const apiRoute = require('./routes/api');
const adminRoute = require('./routes/admin');
mongoose.Promise = global.Promise;
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`listening to port ${port}`));
app.use(bodyParser.json());
app.use('/api', apiRoute);
app.use('/admin', adminRoute);
// As per Mongoose Documentation
const options = {
useNewUrlParser: true,
useCreateIndex: true,
useFindAndModify: false,
useUnifiedTopology: true,
autoIndex: false, // Don't build indexes
//reconnectTries: Number.MAX_VALUE, // Never stop trying to reconnect
//reconnectInterval: 500, // Reconnect every 500ms
poolSize: 10, // Maintain up to 10 socket connections
// If not connected, return errors immediately rather than waiting for reconnect
bufferMaxEntries: 0,
connectTimeoutMS: 10000, // Give up initial connection after 10 seconds
socketTimeoutMS: 45000, // Close sockets after 45 seconds of inactivity
family: 4 // Use IPv4, skip trying IPv6
};
mongoose.connect('mongodb://localhost:27017/admin', options).then(
() => {
console.log('db connected');
},
(err) => {
console.log('connection error');
}
);
This is /routes/api.js
const router = express.Router();
const Ticket = require('../models/Ticket');
const Joi = require('joi');
const { seatValidation, typeValidation } = require('../validation');
//View all tickets
router.get('/tickets', async (req, res) => {
try {
let tickets = await Ticket.find();
res.json(tickets);
} catch (error) {
res.json({ message: error });
}
});
//View Ticket Status
router.get('/status/seat/:seat', async (req, res) => {
// input validation - seat should be a number and between 1-40
let { error } = seatValidation(req.params.seat);
if (error) return res.status(404).send(error);
try {
let status = await Ticket.findOne({ seat: parseInt(req.params.seat) }).select('status');
res.json(status);
} catch (error) {
res.json({ message: error });
}
});
//View Details of person owning the ticket
router.get('/details/seat/:seat', async (req, res) => {
// input validation - seat should be a number and between 1-40
let { error } = seatValidation(req.params.seat);
if (error) return res.status(404).send(error);
try {
let status = await Ticket.findOne({ seat: parseInt(req.params.seat) })
.select('status')
.select('first_name')
.select('last_name')
.select('gender')
.select('email')
.select('mobile');
res.json(status);
} catch (error) {
res.json({ message: error });
}
});
// adding empty seats
router.post('/add', async (req, res) => {
// input validation - seat should be a number and between 1-40
let { error } = seatValidation(req.body);
if (error) return res.status(404).send(error);
let ticket = new Ticket({
seat: req.body.seat,
status: 'open'
});
let ticketSaved = await ticket.save();
try {
res.json(ticketSaved);
} catch (error) {
res.json({ message: error });
}
});
//View all open tickets
router.get('/tickets/open', async (req, res) => {
try {
let tickets = await Ticket.find({ status: 'open' });
res.json(tickets);
} catch (error) {
res.json({ message: error });
}
});
//View all closed tickets
router.get('/tickets/closed', async (req, res) => {
try {
let tickets = await Ticket.find({ status: 'close' });
res.json(tickets);
} catch (error) {
res.json({ message: error });
}
});
//Update the ticket status (open/close + adding user details)
router.put('/status/:seat/:type', async (req, res) => {
// input validation - seat should be a number and between 1-40
let { seatError } = seatValidation(req.params.seat);
if (seatError) return res.status(404).send(seatError);
// input validation - type should be a string and should be 'open' or 'close'
let { typeError } = typeValidation(req.params.type);
if (typeError) return res.status(400).send(typeError);
try {
let type = req.params.type;
if (type === 'open') {
try {
let ticket = await Ticket.updateOne(
{ seat: req.params.seat },
{
$set: { status: 'open' },
$unset: { first_name: 1, last_name: 1, gender: 1, email: 1, mobile: 1 }
}
);
res.json(ticket);
} catch (error) {
res.json({ message: error });
}
} else if (type === 'close') {
// input validation - type should be a string and should be 'open' or 'close'
let ticketSchema = {
first_name: Joi.string().min(3).required(),
last_name: Joi.string().min(3).required(),
gender: Joi.string().valid('M').valid('F').valid('U').required(),
email: Joi.string().email().required(),
mobile: Joi.number().integer().min(1000000000).max(9999999999).required()
};
let validation = Joi.validate(req.body, ticketSchema);
if (validation.error) {
res.status(400).send(validation.error);
return;
}
try {
let ticket = await Ticket.updateOne(
{ seat: req.params.seat },
{
$set: {
status: 'close',
first_name: req.body.first_name,
last_name: req.body.last_name,
gender: req.body.gender,
email: req.body.email,
mobile: req.body.mobile
}
}
);
res.json(ticket);
} catch (error) {
res.json({ message: error });
}
}
} catch (error) {
res.json({ message: error });
}
});
module.exports = router;
This is schema /models/Ticket.js
const mongoose = require('mongoose');
const TicketSchema = mongoose.Schema({
seat: Number,
status: String,
first_name: String,
last_name: String,
gender: String,
email: String,
mobile: Number
});
module.exports = mongoose.model('Ticket', TicketSchema);
Can anyone help me out?
Here is the error from postman:
Postman screenshot
In your index.js file you have specified route for apiRoute as /api and in the screenshot, it appears that you have missed that.
Please use the URL as localhost:3000/api/tickets
instead of just localhost:3000/tickets and it should work correctly.
As you can see as you have missed the /api part the express does not have the path you requested and is returning the status of 404 not found.
I am following this graphql tutorial, everything was going ok until I try to use dataloaders.
My server.js is:
const start = async () => {
const mongo = await connectMongo();
const buildOptions = async req => {
const user = await authenticate(req, mongo.Users);
return {
context: {
dataloaders: buildDataloaders(mongo),
mongo,
user
},
schema
};
};
app.use('/graphql', bodyParser.json(), graphqlExpress(buildOptions));
app.use(
'/graphiql',
graphiqlExpress({
endpointURL: '/graphql',
passHeader: `'Authorization': 'bearer token-name#email.com'`
})
);
app.use('/', expressStaticGzip('dist'));
app.use('/attendance', expressStaticGzip('dist'));
app.use('/login', expressStaticGzip('dist'));
spdy.createServer(sslOptions, app).listen(process.env.PORT || 8080, error => {
if (error) {
console.error(error);
return process.exit(1);
} else {
console.info(
`App available at https://localhost:${process.env.PORT || 3000}`
);
}
});
};
My copy and paste dataloaders.js:
const DataLoader = require('dataloader');
async function batchUsers(Users, keys) {
return await Users.find({ _id: { $in: keys } }).toArray();
}
module.exports = ({ Users }) => ({
userLoader: new DataLoader(keys => batchUsers(Users, keys), {
cacheKeyFn: key => key.toString()
})
});
And my resolvers.js:
export default {
Query: {
allLinks: async (root, data, { mongo: { Links } }) =>
Links.find({}).toArray()
},
Mutation: {
createLink: async (root, data, { mongo: { Links }, user }) => {
const newLink = Object.assign({ postedById: user && user._id }, data);
const response = await Links.insert(newLink);
return Object.assign({ id: response.insertedIds[0] }, newLink);
},
createUser: async (root, data, { mongo: { Users } }) => {
const newUser = {
name: data.name,
email: data.authProvider.email.email,
password: data.authProvider.email.password
};
const response = await Users.insert(newUser);
return Object.assign({ id: response.insertedIds[0] }, newUser);
},
signinUser: async (root, data, { mongo: { Users } }) => {
const user = await Users.findOne({ email: data.email.email });
if (data.email.password === user.password) {
return { token: `token-${user.email}`, user };
}
}
},
Link: {
id: root => root._id || root.id,
postedBy: async ({ postedById }, data, { dataloaders: { userLoader } }) => {
return await userLoader.load(postedById);
}
},
User: {
id: root => root._id || root.id
}
};
When I try get my allLinks I got the error:
TypeError: The loader.load() function must be called with a value,but
got: undefined.
Can anyone help me?
So I was able to reproduce the error by creating a link with a user, deleting the user from the Mongo database, and then querying for the postedBy attribute of the Link.
I would suggest dropping all your links and recreating your user (register + sign in), creating a new link, then querying for the postedBy field.