Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Graphql setup #42

Merged
merged 11 commits into from
Oct 4, 2023
454 changes: 283 additions & 171 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
"dotenv": "^16.0.3",
"eventsource": "^2.0.2",
"express": "^4.14.0",
"express-graphql": "^0.12.0",
"graphql": "^15.8.0",
"mongodb": "^6.0.0",
"mongoose": "^7.5.2",
"morgan": "^1.10.0",
Expand Down
12 changes: 12 additions & 0 deletions src/config/app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import express, { Express } from "express";
import cors from "cors";
import bodyParser from "body-parser";
import { graphqlHTTP } from "express-graphql";
import { schema, resolver } from "./schema/index";

import delayed from "../routes/delayed";
import tickets from "../routes/tickets";
Expand All @@ -16,6 +18,16 @@ app.disable("x-powered-by");
app.use(bodyParser.json()); // for parsing application/json
app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded

// Make handler a graphql handler
app.use(
"/graphql",
graphqlHTTP({
schema,
rootValue: resolver,
graphiql: true
})
);

app.use("/delayed", delayed);
app.use("/tickets", tickets);
app.use("/codes", codes);
Expand Down
11 changes: 11 additions & 0 deletions src/config/schema/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import schema from "./schema";
import query from "./query";
import mutation from "./mutation";

// Spread query and mutation resolvers into a single object
const resolver = {
...query,
...mutation
};

export { resolver, schema };
67 changes: 67 additions & 0 deletions src/config/schema/mutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import ticketRepository from "../../db/ticketRepository";
import type ITicket from "../../models/ITicket";

const mutation = {
createTicket: async ({ code, trainnumber, traindate }: ITicket) => {
const book = ticketRepository.createTicket({ code, trainnumber, traindate });
return {
data: book,
ok: true,
error: ""
};
},

updateTicket: async ({ id, code, trainnumber, traindate }: ITicket & { id: string }) => {
try {
const ticket = await ticketRepository.updateTicket(id, {
code,
trainnumber,
traindate
});
if (!ticket) {
return {
data: null,
ok: false,
error: "Ticket not found"
};
}
return {
data: ticket,
ok: true,
error: ""
};
} catch (err) {
return {
data: null,
ok: false,
error: err.message
};
}
},

deleteTicket: async ({ id }: { id: string }) => {
try {
const ticket = await ticketRepository.deleteTicket(id);
if (!ticket) {
return {
data: null,
ok: false,
error: "Ticket not found"
};
}
return {
data: ticket,
ok: true,
error: ""
};
} catch (err) {
return {
data: null,
ok: false,
error: err.message
};
}
}
};

export default mutation;
12 changes: 12 additions & 0 deletions src/config/schema/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import ticketRepository from "../../db/ticketRepository";

const query = {
tickets: async ({ limit }: { limit: number }) => {
return await ticketRepository.getAllTickets(limit);
},
ticket: async ({ id }: { id: string }) => {
return await ticketRepository.getTicketById(id);
}
};

export default query;
29 changes: 29 additions & 0 deletions src/config/schema/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { buildSchema } from "graphql";

const schema = buildSchema(`
type Query {
tickets(limit: Int): [Ticket]
ticket(id: ID!): Ticket
}
type Mutation {
createTicket(code: String!, trainnumber: String!, traindate: String!): TicketResponse
updateTicket(id: ID!, code: String, trainnumber: String, traindate: String): TicketResponse
deleteTicket(id: ID!): TicketResponse
}
type Ticket {
id: ID!
code: String!
trainnumber: String!
traindate: String!
}
type Tickets {
tickets: [Ticket]
}
type TicketResponse {
data: Ticket
error: String
ok: Boolean
}
`);

export default schema;
39 changes: 38 additions & 1 deletion src/controllers/tickets.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Response, Request } from "express";
import Ticket from "../models/Ticket";
import Ticket from "../db/models/Ticket";
import type ErrorResponse from "../models/ErrorResponse.model";

const tickets = {
Expand Down Expand Up @@ -53,6 +53,43 @@ const tickets = {
}
});
}
},

updateTicket: async function updateTicket(
req: Request,
res: Response
): Promise<object | ErrorResponse> {
try {
const ticket = await Ticket.findById(req.body.id);

if (ticket === null) {
return res.status(404).json({
errors: {
status: 404,
source: "/tickets",
title: "Could not find ticket",
message: "Could not find ticket"
}
});
}

ticket.code = req.body.code;
ticket.trainnumber = req.body.trainnumber;
ticket.traindate = req.body.traindate;

await ticket.save();

return res.status(204).send();
} catch (err) {
res.status(500).json({
errors: {
status: 500,
source: "/tickets",
title: "Database Error",
message: err.message
}
});
}
}
};

Expand Down
File renamed without changes.
37 changes: 37 additions & 0 deletions src/db/ticketRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Ticket from "./models/Ticket";
import type ITicket from "../models/ITicket";

const ticketRepository = {
getAllTickets: async (limit: number) => {
return await Ticket.find({}).limit(limit);
},

getTicketById: async (id: string) => {
return await Ticket.findById(id);
},

createTicket: async ({ code, trainnumber, traindate }: ITicket) => {
const newTicket = new Ticket({ code, trainnumber, traindate });
await newTicket.save();

return newTicket;
},

updateTicket: async (id: string, { code, trainnumber, traindate }: ITicket) => {
const ticket = await Ticket.findById(id);

if (code) ticket.code = code;
if (trainnumber) ticket.trainnumber = trainnumber;
if (traindate) ticket.traindate = traindate;

await ticket.save();

return ticket;
},

deleteTicket: async (id: string) => {
return await Ticket.findByIdAndDelete(id);
}
};

export default ticketRepository;
7 changes: 7 additions & 0 deletions src/models/ITicket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
interface ITicket {
code: string;
trainnumber: string;
traindate: string;
}

export default ITicket;
2 changes: 2 additions & 0 deletions src/routes/tickets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ router.get("/", (req: Request, res: Response) => tickets.getTickets(req, res));

router.post("/", (req: Request, res: Response) => tickets.createTicket(req, res));

router.put("/", (req: Request, res: Response) => tickets.updateTicket(req, res));

export default router;
69 changes: 62 additions & 7 deletions test/tickets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,27 @@ process.env.NODE_ENV = "test";

import chai from "chai";
import chaiHttp from "chai-http";
import mongoose from "mongoose";
import server from "../src/index";

import Ticket from "../src/models/Ticket";
import Ticket from "../src/db/models/Ticket";

chai.should();
chai.use(chaiHttp);

const requestBody = {
const exampleTicket = {
code: "ABC123",
trainnumber: "12345",
traindate: "2023-09-20"
};

// Simply another example to update to when testing put requests
const updatedTicket = {
code: "123ABC",
trainnumber: "54321",
traindate: "2023-10-04"
};

describe("tickets", () => {
describe("GET /tickets", () => {
it("request results in a 200 status code", (done) => {
Expand Down Expand Up @@ -50,7 +58,7 @@ describe("tickets", () => {
it("request results in a 201 status code", (done) => {
chai.request(server)
.post("/tickets")
.send(requestBody)
.send(exampleTicket)
.end((err, res) => {
res.should.have.status(201);
done();
Expand All @@ -60,18 +68,18 @@ describe("tickets", () => {
it("response contains correct json data", (done) => {
chai.request(server)
.post("/tickets")
.send(requestBody)
.send(exampleTicket)
.end((err, res) => {
res.should.have.status(201);
res.should.be.json;
res.body.should.be.a("object");
res.body.should.have.property("data");
res.body.data.should.have.property("id");
res.body.data.should.have.property("code").equal(requestBody.code);
res.body.data.should.have.property("code").equal(exampleTicket.code);
res.body.data.should.have
.property("trainnumber")
.equal(requestBody.trainnumber);
res.body.data.should.have.property("traindate").equal(requestBody.traindate);
.equal(exampleTicket.trainnumber);
res.body.data.should.have.property("traindate").equal(exampleTicket.traindate);

done();
});
Expand Down Expand Up @@ -107,4 +115,51 @@ describe("tickets", () => {
});
});
});

describe("PUT /tickets", () => {
let exampleId: mongoose.Types.ObjectId;

beforeEach(async () => {
await Ticket.deleteMany(); // Drop exisiting tickets

const newTicket = new Ticket(exampleTicket);

await newTicket.save();

exampleId = newTicket._id;
});

it("request results in a 204 status code", (done) => {
chai.request(server)
.put("/tickets")
.send({
id: exampleId,
...exampleTicket
})
.end((err, res) => {
res.should.have.status(204);
done();
});
});

it("fields have been updated correctly", (done) => {
chai.request(server)
.put("/tickets")
.send({
id: exampleId,
...updatedTicket
})
.end(async (err, res) => {
res.should.have.status(204);

const ticket = await Ticket.findById(exampleId);

ticket.should.have.property("code").equal(updatedTicket.code);
ticket.should.have.property("trainnumber").equal(updatedTicket.trainnumber);
ticket.should.have.property("traindate").equal(updatedTicket.traindate);

done();
});
});
});
});