import { Resolver, Query, Mutation, Arg } from "type-graphql";
import * as bcrypt from "bcryptjs";
import { User } from "../../../../entities";
import { getUserEntity } from "../../../../containers";
import { UserAuthInput } from "./UserAuthInput";
import * as jwt from "jsonwebtoken";
import { config } from "../../../../config";
@Resolver(of => User)
class UserResolver {
private readonly _UserEntity = getUserEntity();
@Mutation(returns => User)
async signIn(@Arg("input")
{
emailAddress,
password
}: UserAuthInput): Promise<User> {
const user = await this._UserEntity.findOne({ where: { emailAddress } });
if (!user) {
throw new Error("User not found");
}
if (!bcrypt.compare(password, user.password)) {
throw new Error("Invalid password");
}
user.jwt = jwt.sign({ id: user.id }, config.jwtSecret);
return user;
}
@Mutation(returns => User)
async signUp(@Arg("input")
{
emailAddress,
password
}: UserAuthInput): Promise<User> {
if (await this._UserEntity.findOne({ where: { emailAddress } })) {
throw new Error("User with this email address already exists.");
}
const salt = bcrypt.genSaltSync(10);
try {
const user = await this._UserEntity.create({
emailAddress,
password: bcrypt.hashSync(password, salt)
});
user.jwt = jwt.sign({ id: user.id }, config.jwtSecret);
return user;
} catch (error) {
console.error("Cannot create user.", error);
throw error;
}
}
}
export { UserResolver };
Writing tests
Update setupTests.ts to require the environment variables:
src/setupTests.ts
require('dotenv').config()
When building the schema, GraphQL will complain if we don't have any queries. For this reason, let's create a Health resolver and add it to our schema when testing.
resolvers/Health.ts
import { Int, ObjectType, Query, Resolver, Field } from "type-graphql";
@ObjectType()
class Health {
@Field(() => Int)
ok: number;
}
@Resolver(of => Health)
class HealthResolver {
@Query(returns => Health)
async health(): Promise<Health> {
return { ok: 1 };
}
}
export { HealthResolver };
Finally, test the resolver:
import { ApolloServerBase, gql } from "apollo-server-core";
import {
createTestClient
} from "apollo-server-testing";
import { buildSchema } from "type-graphql";
import { UserResolver } from "./User";
import { setUserEntity } from "../../../../containers";
import { User } from "../../../../entities";
import { HealthResolver } from "..";
import * as jwt from 'jsonwebtoken'
describe("routers/GraphQL/Resolvers/User", () => {
describe("mutation/signIn", () => {
it("should return a jwt for this user", async () => {
const schema = await buildSchema({
resolvers: [HealthResolver, UserResolver]
});
const server = new ApolloServerBase({ schema });
const testClient = createTestClient(server);
const user = { emailAddress: "test1@mock.com", password: "123" };
const findOne = jest.fn();
findOne.mockResolvedValue({ ...user, id: 'userid', });
setUserEntity(({ findOne } as unknown) as typeof User);
const { data, errors } = await testClient.mutate({
mutation: gql`
mutation signIn($input: UserAuthInput!) {
signIn(input: $input) {
id
emailAddress
jwt
}
}
`,
variables: { input: user }
});
expect(errors).toBeUndefined();
expect(data!.signIn).toMatchObject({ id: "userid", emailAddress: user.emailAddress })
expect(jwt.decode(data!.signIn.jwt)).toMatchObject({ id: 'userid' })
});
});
describe("mutation/signUp", () => {
it("should create a user and return the jwt", async () => {
const schema = await buildSchema({
resolvers: [HealthResolver, UserResolver]
});
const server = new ApolloServerBase({ schema });
const testClient = createTestClient(server);
const user = { emailAddress: "test2@mock.com", password: "123" };
const findOne = jest.fn();
const create = jest.fn();
create.mockResolvedValue({ id: "userid", ...user });
findOne.mockResolvedValue(null);
setUserEntity(({ findOne, create } as unknown) as typeof User);
const { data, errors } = await testClient.mutate({
mutation: gql`
mutation signUp($input: UserAuthInput!) {
signUp(input: $input) {
id
emailAddress
jwt
}
}
`,
variables: { input: user }
});
expect(errors).toBeUndefined();
expect(data!.signUp).toMatchObject({ id: "userid", emailAddress: user.emailAddress })
expect(jwt.decode(data!.signUp.jwt)).toMatchObject({ id: 'userid' })
});
});
});
Tests are running in parallel. We recreate the server for each test to avoid conflicts.
Adding security to the links
How do we know which user just sent a request? GraphQL uses context to pass down information to all resolvers and we will use the Authorization header to send the JWT which each request.
Populating the GraphQL context
routers/GraphQL/index.ts
import "reflect-metadata";
import { ApolloServer } from "apollo-server-express";
import { Router } from "express";
import { buildSchema } from "type-graphql";
import * as resolvers from "./resolvers";
import * as jwt from "jsonwebtoken";
import { config } from "../../config";
import { User } from "../../entities";
export interface GraphQLContext {
user?: User;
}
export class GraphQLRouter {
public readonly router = Router();
constructor() {
this.buildSchema();
}
async buildSchema() {
const schema = await buildSchema({
resolvers: Object.values(resolvers),
validate: false
});
const apolloServer = new ApolloServer({
schema,
context: ({ req }): GraphQLContext => {
if (!req.headers.authorization) {
return {};
}
if (req.headers.authorization.slice(0, 7).trim() !== "Bearer") {
return {};
}
try {
return {
user: jwt.verify(
req.headers.authorization.slice(7),
config.jwtSecret
) as User
};
} catch (error) {
return {};
}
}
});
apolloServer.applyMiddleware({ app: this.router, path: "/" });
}
}
Testing
We can mock the context when creating the test server:
const server = new ApolloServerBase({
schema,
context: { user: { id: "userid" } }
});
The backend is now ready to interact with the frontend. We can interact safely with the database and we have enough to implement anything we might need.
Try it yourself, take this token and paste it in .
A lot is going on in this chapter. Have look at this to see all the changes with comments.