Jest not terminating after tests complete successfully - jestjs

I'm working with jest & supertest to test my api endpoints and the tests are passing with no problems; however jest is never exiting with the Done in X amount of time statement that I would normally see. I looked to make sure the --watch flag was not being used and it was not. It seems as if the connection to the server is never ending so jest is left wondering what is next. UserFactory.generate() creates fake user data using the faker library.
I'm at a loss on trying to troubleshoot this. I've followed the recommended strategies on jest's help page with no luck and been digging around in the issue tracker as well but see nothing about similar issues.
This is what I will see on running my test suite:
As you can see the test is ran 7 passes. I'm told that all tests have been run and then morgan shows the POST that has happened. jest never exits so in reality this is failing because it will timeout on any CI sever barring a manual exit.
I have tried using .end() & done() as well as .then() & done() in place of the async/await. They all are returning the same end result and if it were an issue of a promise not resolving jest will error out over an unresolved promise so I am beyond stumped on why this is not terminating as jest normally would.
Has anyone encountered an issue like this before?
user.controller.test.js
import mongoose from 'mongoose';
import request from 'supertest';
import { UserFactory } from '../../__mocks__';
import { User } from '../../modules';
import { config } from '../../utils';
import app from '../../';
const mockRoute = data => request(app).post(`${config.ENDPOINT}/user/sign-up`).send(data);
describe(`POST: /user/sign-up`, () => {
// remove any user data from db prior to running tests.
beforeAll(async () => { await User.remove(); });
test('Returns status 201 on success.', async () => {
// Returns the response object: res.status === { status }
const { status } = await mockRoute(UserFactory.generate());
expect(status).toEqual(201);
});
afterAll(async () => {
// drop connection to the collection
const { users } = mongoose.connection.collections;
await users.drop();
});
});
user/routes.js
import { Router } from 'express';
import validate from 'express-validation';
import { signUp } from './controller';
import valid from './validation'
const routes = new Router();
/**
* 1. Define the route: 'user/signup'.
* 2. Validate the data being provided on the POST
* against valid.signUp object.
* 3. Provide data to signUp controller method for
* creating a user in the database.
*/
routes.post('/user/sign-up', validate(valid.signUp), signUp);
export default routes;
user/controller.js
import HTTPStatus from 'http-status';
import User from './model';
import { config, filterBody } from '../../utils';
export const signUp = async (req, res, next) => {
const filteredBody = filterBody(req.body, config.WHITELIST.users.signUp);
try {
const user = await User.create(filteredBody);
return res.status(HTTPStatus.CREATED).json(user.toAuthJSON());
} catch (e) {
e.status = HTTPStatus.BAD_REQUEST;
return next(e);
}
}
user/model.js
import mongoose, { Schema } from 'mongoose';
import uniqueValidator from 'mongoose-unique-validator';
import { hashSync, compareSync } from 'bcrypt-nodejs';
import jwt from 'jsonwebtoken';
import { config } from '../../utils';
const UserSchema = new Schema({
email: {
type: String,
unique: true,
required: [true, 'Email is required!'],
trim: true,
validate: {
validator(email) {
const emailRegex = /^[-a-z0-9%S_+]+(\.[-a-z0-9%S_+]+)*#(?:[a-z0-9-]{1,63}\.){1,125}[a-z]{2,63}$/i;
return emailRegex.test(email);
},
message: '{VALUE} is not a valid email!',
}
},
password: {
type: String,
required: [true, 'Password is required!'],
trim: true,
minlength: [6, 'Password need to be longer!'],
validate: {
validator(password) {
return password.length >= 6 && password.match(/\d+/g);
},
},
}
}, { timestamps: true })
UserSchema.plugin(uniqueValidator, {
message: '{VALUE} already taken!',
});
UserSchema.pre('save', function(next) {
if (this.isModified('password')) {
this.password = this._hashPassword(this.password);
return next();
}
return next();
});
UserSchema.methods = {
authenticateUser(password) {
return compareSync(password, this.password);
},
_hashPassword(password) {
return hashSync(password);
},
createToken() {
return jwt.sign({ _id: this._id }, config.JWT_SECRET);
},
toAuthJSON() {
return {
_id: this._id,
token: `JWT ${this.createToken()}`,
};
},
toJSON() {
return {
_id: this._id,
username: this.username,
};
},
};
let User;
try {
User = mongoose.model('User');
} catch (e) {
User = mongoose.model('User', UserSchema);
}
export default User;
user/validation.js
import Joi from 'joi';
export default {
signUp: {
body: {
email: Joi.string().email().required(),
password: Joi.string()
.min(6)
.regex(/^(?=.*[0-9])(?=.*[a-zA-Z])([a-zA-Z0-9]+)$/)
.required(),
},
},
};
middlewares.js
import bodyParser from 'body-parser';
import compression from 'compression';
import cors from 'cors';
import morgan from 'morgan';
import { userRoutes } from '../modules';
export default app => {
app.use(compression());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cors());
app.use(morgan('dev'));
// applying api routes last.
app.use('/api/v1', [userRoutes]);
}
index.js
import express from 'express';
import {
database,
config,
middlewares,
} from './utils';
// Create instance of Express.
const app = express();
const MODE = process.env.NODE_ENV;
// Apply middlewares to Express.
middlewares(app);
// Connect to the environment determined database.
database(config.MONGO_URI)
app.listen(config.PORT, err => {
if (err) { return console.error(err); }
console.log(`App running on port: ${config.PORT} in ${MODE} mode.`);
});
export default app;

Figured out the issue was that I was not doing enough in the afterAll() hook.
I made a small repo to reproduce the issue and troubleshoot it from there and this was what needed to be done so that jest could exit on successful completion of the test suite:
afterAll(async () => {
try {
const { todos } = mongoose.connection.collections;
// Collection is being dropped.
await todos.drop()
// Connection to Mongo killed.
await mongoose.disconnect();
// Server connection closed.
await server.close();
} catch (error) {
console.log(`
You did something wrong dummy!
${error}
`);
throw error;
}

My setup has supertest and mongodb-memory-server.
None of the techniques advised here solved the problem for me.
mongodb-memory-server connection ends when it runs alone. But when i combine it with supertest jest doesnt terminate. And jest doesnt show up any open handle error either.
Jest terminated with success only after I did run with --forceExit command.
https://jestjs.io/docs/cli#--forceexit

this is what it worked for me:
afterAll(async () => {
try {
// Connection to Mongo killed.
await mongoose.disconnect();
} catch (error) {
console.log(`
You did something wrong dummy!
${error}
`);
throw error;
}
});

Related

GraphQL Subscriptions is null using Express-GraphQL and graphql-subscriptions

I am using TypeScript and have Server and Client application. Below is the server code.
Server Code
import express, { Express } from "express";
import { graphqlHTTP } from "express-graphql";
import { buildSchema } from "type-graphql";
import { TaskResolver } from "./resolvers/task.resolver";
import { pgDatasource } from "./configs/db.config";
import { SeatBandingResolver } from "./resolvers/seatBanding.resolver";
import { GuestChatResolver } from "./resolvers/guestChat.resolver";
import { RateResolver } from "./resolvers/rate.resolver";
import { YearResolver } from "./resolvers/year.resolver";
import { ImplementationRateResolver } from "./resolvers/implementationRate.resolver";
import { UserResolver } from "./resolvers/user.resolver";
import { ReportResolver } from "./resolvers/report.resolver";
// Subscriptions
const ws = require("ws");
const { useServer } = require("graphql-ws/lib/use/ws");
const { execute, subscribe } = require("graphql");
const main = async () => {
const app: Express = express();
try {
//connect to db
await pgDatasource.initialize();
} catch (err) {
throw err;
}
//build gql schema
let schema = await buildSchema({
resolvers: [
SeatBandingResolver,
GuestChatResolver,
RateResolver,
YearResolver,
ImplementationRateResolver,
UserResolver,
],
validate: false,
// pubSub: new PubSub()
});
let schemaDoc = await buildSchema({
resolvers: [ReportResolver],
validate: false,
});
//ql schema for report
const docServer = graphqlHTTP((req, res) => {
return {
schema: schemaDoc,
graphiql: true,
context: {
req: req,
header: req.headers,
},
};
});
//setting a graphql server instance
const graphqServer = graphqlHTTP((req, res, graphQLParams) => {
return {
schema,
context: {
req: req,
header: req.headers,
},
graphiql: true,
};
});
app.use(cors());
//graphql endpoint : change it to backend
app.use("/graphql", graphqServer);
//for report : change name to google api
app.use("/doc", docServer);
//test route
app.get("/", (req, res) => {
res.json({
message: "Hello world",
});
});
let server = app.listen(3001, () => {
console.log("server started");
const wsServer = new ws.WebSocketServer({
host: "localhost",
// server,
path: "/graphql",
port: 3001,
});
useServer(
{
schema,
execute,
subscribe,
onConnect: (ctx) => {
console.log("Connect");
},
onSubscribe: (ctx, msg) => {
console.log("Subscribe");
},
onNext: (ctx, msg, args, result) => {
console.debug("Next");
},
onError: (ctx, msg, errors) => {
console.error("Error");
},
onComplete: (ctx, msg) => {
console.log("Complete");
},
},
wsServer
);
});
};
//starting a server
main()
.then(async (_) => {
// await addColumn()
})
.catch((err) => {
console.log(err);
});
Subscription Code at Client Side
import { Year } from "../entities/year.entity";
import { NewYear } from "../inputs/addYear.input";
import {
Arg,
Ctx,
Field,
Int,
Mutation,
ObjectType,
Query,
Resolver,
Root,
Subscription,
UseMiddleware,
} from "type-graphql";
import { Request } from "express";
import { Response } from "../helpers/response.helper";
import { Pagination } from "../inputs/pagination.input";
import { isAuth } from "../helpers/auth.helper";
import { PubSub, PubSubEngine } from "graphql-subscriptions";
const pubSub = new PubSub();
#ObjectType()
class MessagePayload {
#Field()
message: string;
}
#Resolver(() => Year)
export class YearResolver {
#Mutation(() => String)
async sendMessage(#Arg("message") message: string): Promise<string> {
console.log("in send subscription");
pubSub.publish("MESSAGE_NOTIFICATION", { message });
return message;
}
//calling the subscription
#Subscription(() => MessagePayload || null, {
topics: "MESSAGE_NOTIFICATION",
})
async receiveMessage(
#Root() root: MessagePayload
): Promise<MessagePayload | null> {
console.log("in publisher");
console.log(root, "in recieve message");
pubSub.asyncIterator("MESSAGE_NOTIFICATION");
return { message: "hello from the subscription" };
}
}
The issue I am facing here is Subscription is not working properly and the data is always null.
Can anyone help me to identify what I am missing here?
Thanks.
I'm not sure for 100% because your code descriptions are kinda confusing, but it looks like you should return pubSub.asyncIterator('MESSAGE_NOTIFICATION') in method receiveMessage. This method is called to start streaming messages to client at selected channel (MESSAGE_NOTIFICATION), not sending them. To send messages use pubsub. Of course you should change typing too.
You can find a similiar implementation here.

Learn about apollo-graphql Subscription

I'm presently studying Graphql(apollo) and utilising Node.js for the backend.
I've made some progress with queries and mutations, but I'm stuck at subscription. I've seen several videos and read some blogs, but I'm having trouble grasping it since they're utilising front-end frameworks, such as react, and I'm not familiar with react or any other front-end javascript frameworks.
I'm solely interested in learning it for the backend.
Is there anyone who can assist me with this?
I've got three separate queries (or whatever they're called) that I'm working on.
User,
Post and
Comment
Now I want to generate a subscription whenever someone adds a new comment or creates a new post.
For users, comments, and posts, I already have a mutation for add, update and remove.
There is currently no authentication or authorization in place.
Your question is more to help me implement this rather than actually asking an issue or problem you faced after trying out some steps in series of order.
This might help you when learning and implementing GraphQL subscription;
https://www.apollographql.com/docs/apollo-server/data/subscriptions/
https://www.youtube.com/watch?v=R2IrBI4-7Kg&t=698s
If you have tried something and faced a wall, please add your code and what you have done, so I could help you on where you are wrong or what you could do hopefully. Cheers.
The solutions I'm providing are crafted on #apollo/server v.4, with expressMiddleware and mongodb/mongoose on the backend and subscribeToMore with updateQuery on the client-side instead of the useSubscription hook.
The graphql-transport-ws transport library is no longer supported; instead, use graphql-ws.
The implementation differentiates three main collections: User, Post, and Comment, as well as subscription for post creation, post modification, and user authentication.
Likewise, as of 12.2022, the following setup and configuration apply.
Subscription on the backend:
Install the following dependencies:
$ npm i #apollo/server #graphql-tools/schema graphql-subscriptions graphql-ws ws cors body-parser mongoose graphql express
I'm assuming you've already configured your MongoDB models; if not, you might want to look at this repo for a basic setup.
Set up schema types and resolvers, such as this one.
// typeDefs.js
const typeDefs = `#graphql
type User {
id: ID!
email: String!
posts: [Post]!
commentsMade: [Comment]!
}
type Token {
value: String!
}
input UpdatePostInput {
title: String!
}
type Post {
id: ID!
title: String!
postedBy: User
comments: [Comment]!
}
input CommentInput {
text: String!
}
type Comment {
id: ID!
text: String!
commenter: User!
commentFor: Post!
}
type Query {
users: [User]!
user(id: ID!): User!
posts: [Post!]!
post(id: ID!): Post!
comment(id: ID!): Comment!
comments: [Comment!]!
}
type Mutation {
signup(email: String!, password: String!): User
signin(email: String!, password: String!): Token
createPost(title: String): Post
createComment(postId: String!, commentInput: CommentInput!): Comment
updatePost(postId: ID!, updatePostInput: UpdatePostInput!): Post
}
type Subscription {
postAdded: Post
commentAdded: Comment
postUpdated: Post
}
`
export default typeDefs
// resolvers.js
import dotenv from 'dotenv'
import { PubSub } from 'graphql-subscriptions'
import mongoose from 'mongoose'
import { GraphQLError } from 'graphql'
import bcrypt from 'bcrypt'
import UserModel from '../models/User.js'
import PostModel from '../models/Post.js'
import CommentModel from '../models/Comment.js'
dotenv.config()
...
const pubsub = new PubSub()
const User = UserModel
const Post = PostModel
const Comment = CommentModel
const secret = process.env.TOKEN_SECRET
const resolvers = {
Query: {...},
Mutation: {
...
createPost: async (_, args, contextValue) => {
const authUser = contextValue.authUser
if (!authUser) {
throw new GraphQLError('User is not authenticated', {
extensions: {
code: 'UNAUTHENTICATED',
http: { status: 401 },
},
})
}
const post = new Post({
...args,
postedBy: mongoose.Types.ObjectId(authUser.id),
})
try {
const savedPost = await post.save()
authUser.posts = authUser.posts.concat(post._id)
await authUser.save()
const addedPost = {
id: savedPost.id,
title: savedPost.title,
postedBy: savedPost.postedBy,
comments: savedPost.comments,
}
// subscription postAdded with object iterator POST_ADDED
pubsub.publish('POST_ADDED', { postAdded: addedPost })
return post
} catch (error) {
throw new GraphQLError(`Error: ${error.message}`, {
extensions: {
code: 'BAD_USER_INPUT',
http: { status: 400 },
argumentName: args,
},
})
}
},
updatePost: async (_, args, contextValue) => {
const authUser = contextValue.authUser
if (!authUser) {
throw new GraphQLError('User is not authenticated', {
extensions: {
code: 'UNAUTHENTICATED',
http: { status: 401 },
},
})
}
try {
const post = await Post.findByIdAndUpdate(
args.postId,
args.updatePostInput,
{ new: true }
)
.populate('comments')
.populate('postedBy')
const updatedPost = {
id: post.id,
title: post.title,
postedBy: post.postedBy,
comments: post.comments,
}
// subscription postUpdated with object iterator POST_UPDATED
pubsub.publish('POST_UPDATED', { postUpdated: updatedPost })
return post
} catch (error) {
throw new GraphQLError(`Error: ${error.message}`, {
extensions: {
code: 'BAD_REQUEST',
http: { status: 400 },
argumentName: args,
},
})
}
},
},
// resolvers for post addition and post modification using subscribe function
Subscription: {
postAdded: {
subscribe: () => pubsub.asyncIterator('POST_ADDED'),
},
commentAdded: {
subscribe: () => pubsub.asyncIterator('COMMENT_ADDED'),
},
postUpdated: {
subscribe: () => pubsub.asyncIterator('POST_UPDATED'),
},
},
//Hard-coding the default resolvers is appropriate in some cases,
// but I think it is required in fields with references to other
//database models to avoid returning null field values.
Post: {
id: async (parent, args, contextValue, info) => {
return parent.id
},
title: async (parent) => {
return parent.title
},
postedBy: async (parent) => {
const user = await User.findById(parent.postedBy)
.populate('posts', { id: 1, title: 1, comments: 1, postedBy: 1 })
.populate('commentsMade')
//console.log('id', user.id)
//console.log('email', user.email)
return user
},
comments: async (parent) => {
return parent.comments
},
},
Comment: {
id: async (parent, args, contextValue, info) => {
return parent.id
},
text: async (parent, args, contextValue, info) => {
return parent.text
},
commenter: async (parent, args, contextValue, info) => {
const user = await User.findById(parent.commenter)
.populate('posts')
.populate('commentsMade')
return user
},
commentFor: async (parent, args, contextValue, info) => {
const post = await Post.findById(parent.commentFor)
.populate('comments')
.populate('postedBy')
return post
},
},
...
}
export default resolvers
The code in the main entry server file (e.g. index.js) may look like this, e.g.
import dotenv from 'dotenv'
import { ApolloServer } from '#apollo/server'
import { expressMiddleware } from '#apollo/server/express4'
import { ApolloServerPluginDrainHttpServer } from '#apollo/server/plugin/drainHttpServer'
import { makeExecutableSchema } from '#graphql-tools/schema'
import { WebSocketServer } from 'ws'
import { useServer } from 'graphql-ws/lib/use/ws'
import express from 'express'
import http from 'http'
import cors from 'cors'
import bodyParser from 'body-parser'
import jwt from 'jsonwebtoken'
import UserModel from './models/User.js'
import typeDefs from './tpeDefs.js'
import resolvers from './resolvers.js'
import mongoose from 'mongoose'
dotenv.config()
mongoose.set('strictQuery', false)
let db_uri
if (process.env.NODE_ENV === 'development') {
db_uri = process.env.MONGO_DEV
}
mongoose.connect(db_uri).then(
() => {
console.log('Database connected')
},
(err) => {
console.log(err)
}
)
const startGraphQLServer = async () => {
const app = express()
const httpServer = http.createServer(app)
const schema = makeExecutableSchema({ typeDefs, resolvers })
const wsServer = new WebSocketServer({
server: httpServer,
path: '/',
})
const serverCleanup = useServer({ schema }, wsServer)
const server = new ApolloServer({
schema,
context: async ({ req }) => {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1]
if (token) {
const decoded = jwt.verify(token, process.env.TOKEN_SECRET)
const authUser = await UserModel.findById(decoded.id)
.populate('commentsMade')
.populate('posts')
return { authUser }
}
},
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose()
},
}
},
},
],
})
await server.start()
app.use(
'/',
cors(),
bodyParser.json(),
expressMiddleware(server, {
context: async ({ req }) => {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1]
if (token) {
const decoded = jwt.verify(token, process.env.TOKEN_SECRET)
const authUser = await UserModel.findById(decoded.id)
.populate('commentsMade')
.populate('posts')
return { authUser }
}
},
})
)
const PORT = 4000
httpServer.listen(PORT, () =>
console.log(`Server is now running on http://localhost:${PORT}`)
)
}
startGraphQLServer()
END. It's time to run some tests and checks in the Apollo Explorer sandbox. Be conscientious about defining the required default resolvers to avert the Apollo server from sending null values on your behalf.
To view the code and implementation, go to this repository.
Happy coding!

Next.js / Node.js (Express): Set Cookies (with httpOnly) are in the response header but not in the browser storage

Server: Node.js, express, Type-Graphql with Apollo Server
In index.ts:
import 'reflect-metadata';
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { buildSchema } from 'type-graphql';
import { createConnection } from 'typeorm';
import { verify } from 'jsonwebtoken';
import coockieParser from 'cookie-parser';
import cors from 'cors';
import User from './entity/User';
import UserResolver from './resolvers';
import { createAccessToken, createRefreshToken, sendRefreshToken } from './auth';
require('dotenv').config();
const corsOptions = {
allowedHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'X-Access-Token', 'Authorization'],
credentials: true, // this allows to send back (to client) cookies
methods: 'GET,HEAD,OPTIONS,PUT,PATCH,POST,DELETE',
origin: 'http://localhost:3000',
preflightContinue: false,
};
(async () => {
const PORT = process.env.PORT || 4000;
const app = express();
app.use(coockieParser());
app.use(cors(corsOptions));
// -- non graphql endpoints
app.get('/', (_, res) => {
res.send('Starter endpoint');
});
app.post('/refresh_token', async (req, res) => {
const token = req.cookies.jid;
if (!token) {
return res.send({ ok: false, accessToken: '' });
}
let payload: any = null;
try {
payload = verify(token, process.env.REFRESH_TOKEN_SECRET!);
} catch (e) {
console.log(e);
return res.send({ ok: false, accessToken: '' });
}
// token is valid, and the access token can be send back
const user = await User.findOne({ id: payload.userId });
if (!user) {
return res.send({ ok: false, accessToken: '' });
}
if (user.tokenVersion !== payload.tokenVersion) {
return res.send({ ok: false, accessToken: '' });
}
sendRefreshToken(res, createRefreshToken(user));
return res.send({ ok: true, accessToken: createAccessToken(user) });
});
//--
// -- db
await createConnection();
// --
// -- apollo server settings
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [UserResolver],
}),
context: ({ req, res }) => ({ req, res }),
});
await apolloServer.start();
apolloServer.applyMiddleware({
app,
cors: false,
});
// --
app.listen(PORT, () => {
console.log(`Server running on port: ${PORT}`);
});
})();
Login mutation in the UserResolver:
//..
#Mutation(() => LoginResponse)
async login(
#Arg('email') email: string,
#Arg('password') password: string,
#Ctx() { res }: AuthContext,
): Promise<LoginResponse> {
const user = await User.findOne({ where: { email } });
if (!user) {
throw new Error('Incorrect email');
}
const valid = await compare(password, user.password);
if (!valid) {
throw new Error('Incorrect password');
}
sendRefreshToken(res, createRefreshToken(user));
return {
accessToken: createAccessToken(user),
user,
};
}
//..
When handling authentification, the cookies are set in the response header as follows:
//..
export const createAccessToken = (user: User) => sign({ userId: user.id }, process.env.ACCESS_TOKEN_SECRET!, { expiresIn: '10m' });
export const createRefreshToken = (user: User) => sign({ userId: user.id, tokenVersion: user.tokenVersion }, process.env.REFRESH_TOKEN_SECRET!, { expiresIn: '7d' });
export const sendRefreshToken = (res: Response, refreshToken: string) => {
res.cookie('jid', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/refresh_token',
});
};
//..
Client: Next.js, Graphql with URQL
In _app.tsx:
/* eslint-disable react/jsx-props-no-spreading */
import * as React from 'react';
import Head from 'next/head';
import { AppProps } from 'next/app';
import { ThemeProvider } from '#mui/material/styles';
import CssBaseline from '#mui/material/CssBaseline';
import { CacheProvider, EmotionCache } from '#emotion/react';
import { createClient, Provider } from 'urql';
import theme from '../styles/theme';
import createEmotionCache from '../lib/createEmotionCache';
import '../styles/globals.css';
// Client-side cache shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();
interface IAppProps extends AppProps {
// eslint-disable-next-line react/require-default-props
emotionCache?: EmotionCache;
}
const client = createClient({
url: 'http://localhost:4000/graphql',
fetchOptions: {
credentials: 'include',
},
});
const App = (props: IAppProps) => {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
return (
<Provider value={client}>
<CacheProvider value={emotionCache}>
<Head>
<title>Client App</title>
</Head>
<ThemeProvider theme={theme}>
<CssBaseline />
<Component {...pageProps} />
</ThemeProvider>
</CacheProvider>
</Provider>
);
};
export default App;
Login page does not rely on SSR or SSG (so it is CSR):
import React from 'react';
import LoginForm from '../components/LoginForm/LoginForm';
import Layout from '../layouts/Layout';
interface ILoginProps {}
const Login: React.FC<ILoginProps> = () => (
<Layout
showNavbar={false}
showTransition={false}
maxWidth='xs'
>
<LoginForm />
</Layout>
);
export default Login;
The mutation is used in the LoginForm component to request an access token and set refresh token in the browser cookies:
import React from 'react';
import { useRouter } from 'next/router';
import { useLoginMutation } from '../../generated/graphql';
//...
const LoginForm = () => {
//..
const [, login] = useLoginMutation();
const router = useRouter();
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (disabledSubmit) {
setShowFormHelper(true);
} else {
const res = await login({
email, // from the state of the component
password,
});
if (res && res.data?.login) {
console.log(res.data.login.accessToken);
router.push('/home');
setShowFormHelper(false);
} else {
setHelper('Something went wrong');
}
}
};
//..
};
export default LoginForm;
Issue
So, the problem is that the login response has set-cookie in the header, but the cookie still isn't set in the browser:
Question
Previously, I've implemented the same authentication scheme using the same server code but create-react-app on the client. Everything worked just fine. So, why isn't it working now with next.js? What am I missing?
Work Around
I can use something like cookies-next to put cookies into the storage. The refresh token then would need to be passed in the response data:
import React from 'react';
import { useRouter } from 'next/router';
import { useLoginMutation } from '../../generated/graphql';
import { setCookies } from 'cookies-next';
//...
const LoginForm = () => {
//..
const [, login] = useLoginMutation();
const router = useRouter();
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (disabledSubmit) {
setShowFormHelper(true);
} else {
const res = await login({
email, // from the state of the component
password,
});
if (res && res.data?.login) {
console.log(res.data.login.accessToken);
setCookies('jid', res.data.login.refreshToken);
router.push('/home');
setShowFormHelper(false);
} else {
setHelper('Something went wrong');
}
}
};
//..
};
export default LoginForm;
setCookie accepts options. However, the httpOnly can't be set to true in this case anyway.
Updates
It turns out everything above works in Firefox, but not in Chrome.
in res.cookie defined in the express server, use sameSite:'lax' instead of strict. this may solve the issue.

How to set cookies using apollo server express

I am using apollo server express on my backend and next js on frontend.
I have been trying to set cookies in backend using (response) from context but unfortunately it is not working. i would like your help.
here is my backend code.
import http from 'http';
import { ApolloServer } from 'apollo-server-express';
import express from 'express';
import cors from 'cors';
import { makeExecutableSchema } from '#graphql-tools/schema';
import { ApolloServerPluginDrainHttpServer } from 'apollo-server-core';
import { execute, subscribe } from 'graphql';
import {
SubscriptionServer,
ConnectionParams,
ConnectionContext,
} from 'subscriptions-transport-ws';
import { connect } from './Config';
import { typeDefs } from './TypeDefs';
import { resolvers } from './Resolvers';
import { isAuthForSubscription } from './Helpers';
import cookieParser from 'cookie-parser';
const startServer = async () => {
const app = express();
app.use(
cors({
credentials: true,
origin: 'http://localhost:3000',
})
);
app.use(cookieParser());
const httpServer = http.createServer(app);
const subscriptionServer = SubscriptionServer.create(
{
schema: makeExecutableSchema({ typeDefs, resolvers }),
execute,
subscribe,
onConnect: async (
connectionParams: ConnectionParams,
_websocket: any,
context: ConnectionContext
) => {
let user = null;
if (connectionParams.Authorization) {
user = await isAuthForSubscription(connectionParams.Authorization);
}
return { context, user };
},
},
{
server: httpServer,
path: '/graphql',
}
);
const server = new ApolloServer({
schema: makeExecutableSchema({ typeDefs, resolvers }),
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
serverWillStart: async () => {
return {
drainServer: async () => {
subscriptionServer.close();
},
};
},
},
],
context: ({ req, res }) => ({ req, res }),
});
await server.start();
server.applyMiddleware({ app });
await new Promise<void>(resolve =>
httpServer.listen({ port: 4000 }, resolve)
);
await connect();
console.log(
`server started on port http://localhost:4000${server.graphqlPath}`
);
return { server, app };
};
startServer();
// and my resolver is this one
import { ApolloError } from 'apollo-server-express';
import {
TCreateAccountArgs,
TLoginArgs,
TContext,
CookieName,
} from '../../__generated__';
import { generate } from '../../Helpers';
import { userModel } from '../../Models';
import { CookieOptions } from 'express';
export const login = async(_: any, args: TLoginArgs, { res, req }: TContext) => {
try {
const find = await userModel.findOne({ username: args.username });
if (!find) {
return new ApolloError('Account not found');
}
const comparePassword = generate.comparePassword(
find.password,
args.password
);
if (!comparePassword) {
return new ApolloError('Incorrect password or username');
}
const generateToken = generate.generateToken({ _id: find._id });
const cookieOptions: CookieOptions = {
httpOnly: true,
maxAge: 1 * 60 * 60 * 24 * 1000,
secure: true
};
res.cookie(CookieName.token, generateToken, cookieOptions);
return {
token: generateToken,
data: {
username: find.username,
_id: find._id,
email: find.email,
profilePicture: find.profilePicture,
createdAt: find.createdAt,
updatedAt: find.updatedAt,
},
};
} catch (error: any) {
return new ApolloError(
`Unable to login due to internal server error ${error.message}`
);
}
};
on frontend I am receiving this error message. Cannot read property 'cookie' of undefined
This question has been there from several months ago.
Just in case you haven't been able to fix it yet, the reason why you're not seeing the cookies in the frontend is that you're using the option httpOnly, those cookies are not supposed to be readable by the browser (they are useful to handle authentication in the server while protecting you from cross site scripting).
If you really need to read these cookies in the browser, you should set the httpOnly option to false.

TypeError: Cannot read property 'shawarmaOrdered' of undefined

I am new to GraphQL and mongoose and trying to create a function in a resolver which creates a shawarma order, the inputs required are the shawarma ID, the quantity and an address, when I run the code typed below, I get an Error which states
TypeError: Cannot read property 'shawarmaOrdered' of undefined
the resolver code:
import mongoose from 'mongoose';
import AuthenticationError from 'apollo-server';
import {shawarma} from '../models/shawarmaModel';
export default {
Query:{
},
Mutation: {
createOrder: async(parent, {OrderInput}, {models: {orderModel, shawarmaModel}}, info) => {
console.log('reached1')
try{
const {shawarmaOrdered, quantity, address} = OrderInput;
const order = await orderModel.updateOne({shawarmaOrdered, quantity, address})
return order
} catch(err){
console.log('errorr')
console.log(err)
}
}
},
Order: {
shawarmaOrdered: async(parent, {shawarmaOrdered}, {models:shawarmaModel}, info) =>{
return shawarmaOrdered = shawarmaModel.find((shawarma)=>{
return shawarma.id == parent.id
})
}
}
the schema code:
import {gql} from 'apollo-server'
export default gql`
type Order{
id: ID!
shawarmaOrdered: Shawarma!
quantity: Int
orderedTo: String
}
input OrderInput{
shawarmaOrdered: ID!
quantity: Int
orderedTo: String
}
extend type Query {
order(id: ID!): Order!
}
extend type Mutation {
createOrder(shawarmaOrdered: String!, quantity: Int!, orderedTo: String!): Order!
}
`
the order model code:
import mongoose, { Mongoose } from 'mongoose'
import shawarma from './shawarmaModel'
const orderSchema = new mongoose.Schema({
shawarmaOrdered: {
type: mongoose.Schema.Types.ObjectId,
ref: shawarma
},
quantity: {
type: Number
},
completed: {
type: Boolean,
default: false
}
})
const order = mongoose.model('order', orderSchema)
export default order;
kindly let me know if I'm doing something wrong
So here's how I fixed the problem.
in the index.js file
import cors from 'cors';
import express from 'express';
import jwt from 'jsonwebtoken';
import mongoose from 'mongoose';
import { ApolloServer, AuthenticationError } from 'apollo-server-express';
import schemas from './schemas';
import resolvers from './resolvers';
import userModel from './models/userModel';
import orderModel from './models/orderModel';
import shawarmaModel from './models/shawarmaModel';
const app = express();
app.use(cors());
const getUser = async (req) => {
const token = req.headers['token'];
// console.log(token)
if (token) {
try {
return await jwt.verify(token, 'riddlemethis');
} catch (e) {
console.log(e)
throw new AuthenticationError('Your session expired. Sign in again.');
}
}
};
const server = new ApolloServer({
typeDefs: schemas,
resolvers,
context: async ({ req }) => {
if (req) {
const me = await getUser(req);
return {
me,
models: {
userModel,
orderModel, //this was where the orderModel was misspelled as OrderModel
shawarmaModel
},
};
}
},
});
server.applyMiddleware({ app, path: '/graphql' });
app.listen(5000, async () => {
await mongoose.connect('mongodbconnectionString')
});
in the code above, I have commented the section in which the error was from. Carelessly, when debugging I overlooked checking this file. my mistake. I apologise for not including this code in the original question

Resources