diff --git a/.env.example b/.env.example deleted file mode 100644 index 60f3a816..00000000 --- a/.env.example +++ /dev/null @@ -1,6 +0,0 @@ -DATABASE_URL="YOUR_DB_URL" - -# We need the following URL environment variable for test purposes: -# - TEST_DATABASE_URL must be a **completely separate** database from any other used in this file - -TEST_DATABASE_URL="YOUR_TEST_DB_URL" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dd9b27f1..697117ea 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,22 +12,22 @@ datasource db { // https://www.prisma.io/docs/concepts/components/prisma-schema/data-model model Customer { - id Int @id @default(autoincrement()) - name String - contact Contact? - tickets Ticket[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id Int @id @default(autoincrement()) + name String + contact Contact? + tickets Ticket[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model Contact { - id Int @id @default(autoincrement()) - customer Customer @relation(fields: [customerId], references: [id]) - customerId Int @unique - phone String - email String @unique - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id Int @id @default(autoincrement()) + customer Customer @relation(fields: [customerId], references: [id]) + customerId Int @unique + phone String + email String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model Movie { @@ -40,31 +40,31 @@ model Movie { } model Screen { - id Int @id @default(autoincrement()) - number Int - screenings Screening[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id Int @id @default(autoincrement()) + number Int + screenings Screening[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model Screening { - id Int @id @default(autoincrement()) - tickets Ticket[] - movie Movie @relation(fields: [movieId], references: [id]) - movieId Int - screen Screen @relation(fields: [screenId], references: [id]) - screenId Int - startsAt DateTime - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id Int @id @default(autoincrement()) + tickets Ticket[] + movie Movie @relation(fields: [movieId], references: [id]) + movieId Int + screen Screen @relation(fields: [screenId], references: [id]) + screenId Int + startsAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model Ticket { - id Int @id @default(autoincrement()) - screening Screening @relation(fields: [screeningId], references: [id]) - screeningId Int - customer Customer @relation(fields: [customerId], references: [id]) - customerId Int - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id Int @id @default(autoincrement()) + screening Screening @relation(fields: [screeningId], references: [id]) + screeningId Int + customer Customer @relation(fields: [customerId], references: [id]) + customerId Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } diff --git a/src/controllers/customer.js b/src/controllers/customer.js index 775cfb42..adae6a32 100644 --- a/src/controllers/customer.js +++ b/src/controllers/customer.js @@ -1,48 +1,40 @@ -const { PrismaClientKnownRequestError } = require("@prisma/client") -const { createCustomerDb } = require('../domains/customer.js') +const { PrismaClientKnownRequestError } = require("@prisma/client"); +const { createCustomerDb } = require("../domains/customer.js"); const createCustomer = async (req, res) => { - const { - name, - phone, - email - } = req.body + const { name, phone, email } = req.body; if (!name || !phone || !email) { return res.status(400).json({ - error: "Missing fields in request body" - }) + error: "Missing fields in request body", + }); } - - // Try-catch is a very common way to handle errors in JavaScript. - // It allows us to customise how we want errors that are thrown to be handled. - // Read more here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch - - // Here, if Prisma throws an error in the process of trying to create a new customer, - // instead of the Prisma error being thrown (and the app potentially crashing) we exit the - // `try` block (bypassing the `res.status` code) and enter the `catch` block. try { - const createdCustomer = await createCustomerDb(name, phone, email) + const createdCustomer = await createCustomerDb(name, phone, email); - res.status(201).json({ customer: createdCustomer }) + res.status(201).json({ customer: createdCustomer }); } catch (e) { - // In this catch block, we are able to specify how different Prisma errors are handled. - // Prisma throws errors with its own codes. P2002 is the error code for - // "Unique constraint failed on the {constraint}". In our case, the {constraint} is the - // email field which we have set as needing to be unique in the prisma.schema. - // To handle this, we return a custom 409 (conflict) error as a response to the client. - // Prisma error codes: https://www.prisma.io/docs/orm/reference/error-reference#common - // HTTP error codes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses if (e instanceof PrismaClientKnownRequestError) { if (e.code === "P2002") { - return res.status(409).json({ error: "A customer with the provided email already exists" }) + return res + .status(409) + .json({ error: "A customer with the provided email already exists" }); } } - res.status(500).json({ error: e.message }) + res.status(500).json({ error: e.message }); } -} +}; +const updateCustomer = async (req, res) => { + const paramsId = Number(req.params.id); + const { name, contact } = req.body; + const customer = await updateCostumerDb(paramsId, name, contact); + res.status(201).json({ + customer, + }); +}; module.exports = { - createCustomer -} + createCustomer, + updateCustomer, +}; diff --git a/src/controllers/movies.js b/src/controllers/movies.js new file mode 100644 index 00000000..3ec09b92 --- /dev/null +++ b/src/controllers/movies.js @@ -0,0 +1,71 @@ +const { + createMovieDb, + getAllMoviesDb, + findMovieByIdDb, + updateMovieByIdDb, +} = require("../domains/movies.js"); +const { NotFoundError } = require("@prisma/client"); + +const getAllMovies = async (req, res) => { + try { + const { runtimeLt, runtimeGt } = req.query; + const movies = await getAllMoviesDb(Number(runtimeLt), Number(runtimeGt)); + res.status(200).json({ movies }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +const createMovie = async (req, res) => { + try { + const { title, runtimeMins } = req.body; + if (!title || !runtimeMins) { + return res.status(400).json({ error: "Missing fields in request body" }); + } + const createdMovie = await createMovieDb(title, runtimeMins); + res.status(201).json({ movie: createdMovie }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +const findMovieById = async (req, res) => { + try { + const { id } = req.params; + const searchedMovie = await findMovieByIdDb(id); + if (!searchedMovie) { + return res.status(404).json({ error: "Movie not found" }); + } + res.status(200).json({ movie: searchedMovie }); + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).json({ error: error.message }); + } else { + res.status(500).json({ error: error.message }); + } + } +}; + +const updateMovieById = async (req, res) => { + try { + const { id } = req.params; + const { title, runtimeMins } = req.body; + if (!title || !runtimeMins) { + return res.status(400).json({ error: "Missing fields in request body" }); + } + const updatedMovie = await updateMovieByIdDb( + Number(id), + title, + runtimeMins + ); + res.status(201).json({ movie: updatedMovie }); + } catch (error) { + if (error.code === "P2015") { + res.status(404).json({ error: "Movie not found" }); + } else { + res.status(500).json({ error: error.message }); + } + } +}; + +module.exports = { getAllMovies, createMovie, findMovieById, updateMovieById }; diff --git a/src/controllers/screens.js b/src/controllers/screens.js new file mode 100644 index 00000000..b20cad01 --- /dev/null +++ b/src/controllers/screens.js @@ -0,0 +1,33 @@ +const { createScreenDb } = require("../domains/screens.js"); +const { PrismaClientKnownRequestError } = require("@prisma/client"); + +const createScreen = async (req, res) => { + const { number, screenings } = req.body; + + try { + if (!number) { + return res + .status(400) + .json({ error: "Missing number field in request body" }); + } + + const screenNumber = Number(number); + + const createdScreen = await createScreenDb(screenNumber, screenings); + + res.status(201).json({ screen: createdScreen }); + } catch (error) { + if (error instanceof PrismaClientKnownRequestError) { + if (error.code === "P2002") { + return res + .status(409) + .json({ error: "A screen with the provided number already exists" }); + } + } + + console.error("Error creating screen:", error); + res.status(500).json({ error: "Failed to create screen" }); + } +}; + +module.exports = { createScreen }; diff --git a/src/domains/customer.js b/src/domains/customer.js index c7f315fd..21fc9672 100644 --- a/src/domains/customer.js +++ b/src/domains/customer.js @@ -1,26 +1,46 @@ -const prisma = require('../utils/prisma') +const prisma = require("../utils/prisma"); /** * This will create a Customer AND create a new Contact, then automatically relate them with each other * @tutorial https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries#create-a-related-record */ -const createCustomerDb = async (name, phone, email) => await prisma.customer.create({ - data: { - name, - contact: { - create: { - phone, - email - } - } - }, - // We add an `include` outside of the `data` object to make sure the new contact is returned in the result - // This is like doing RETURNING in SQL - include: { - contact: true - } -}) +const createCustomerDb = async (name, phone, email) => + await prisma.customer.create({ + data: { + name, + contact: { + create: { + phone, + email, + }, + }, + }, + // We add an `include` outside of the `data` object to make sure the new contact is returned in the result + // This is like doing RETURNING in SQL + include: { + contact: true, + }, + }); +const updateCustomer = async (id, name, phone, email) => + await prisma.customer.update({ + where: { + id: id, + }, + data: { + name: name, + contact: { + update: { + phone: phone, + email: email, + }, + }, + }, + include: { + contact: true, + }, + }); module.exports = { - createCustomerDb -} + createCustomerDb, + updateCustomer, +}; diff --git a/src/domains/movies.js b/src/domains/movies.js new file mode 100644 index 00000000..8058711d --- /dev/null +++ b/src/domains/movies.js @@ -0,0 +1,82 @@ +const prisma = require("../utils/prisma"); + +const createMovieDb = async (title, runtimeMins, screenings) => { + const data = { + title, + runtimeMins, + }; + + if (screenings) { + data.screenings = { + createMany: { + data: screenings, + }, + }; + } + + const createdMovie = await prisma.movie.create({ + data, + include: { + screenings: true, + }, + }); + + return createdMovie; +}; + +const getAllMoviesDb = async () => { + const movies = await prisma.movie.findMany({ + include: { + screenings: {}, + }, + }); + + return movies; +}; + +const findMovieByIdDb = async (id) => { + const parsedId = Number(id); + + if (isNaN(parsedId)) { + return findMovieByTitleDb(id); + } + + const movie = await prisma.movie.findUniqueOrThrow({ + where: { id: parsedId }, + include: { + screenings: true, + }, + }); + + return movie; +}; + +const findMovieByTitleDb = async (title) => { + const movie = await prisma.movie.findUniqueOrThrow({ + where: { title }, + include: { + screenings: true, + }, + }); + + return movie; +}; + +const updateMovieByIdDb = async (id, title, runtimeMins) => { + const updatedMovie = await prisma.movie.update({ + where: { id }, + data: { title, runtimeMins }, + include: { + screenings: true, + }, + }); + + return updatedMovie; +}; + +module.exports = { + createMovieDb, + getAllMoviesDb, + findMovieByIdDb, + updateMovieByIdDb, +}; diff --git a/src/domains/screens.js b/src/domains/screens.js new file mode 100644 index 00000000..4135403e --- /dev/null +++ b/src/domains/screens.js @@ -0,0 +1,30 @@ +const prisma = require("../utils/prisma"); + +const createScreenDb = async (number, screenings) => { + const data = { + number, + }; + + if (screenings) { + data.screenings = { + createMany: { + data: screenings, + }, + }; + } + + try { + const createdScreen = await prisma.screen.create({ + data, + include: { + screenings: true, + }, + }); + + return createdScreen; + } catch (error) { + throw new Error(`Failed to create screen: ${error.message}`); + } +}; + +module.exports = { createScreenDb }; diff --git a/src/routers/movies.js b/src/routers/movies.js new file mode 100644 index 00000000..dd324c65 --- /dev/null +++ b/src/routers/movies.js @@ -0,0 +1,16 @@ +const express = require("express"); +const { + getAllMovies, + createMovie, + findMovieById, + updateMovieById, +} = require("../controllers/movies"); + +const router = express.Router(); + +router.get("/", getAllMovies); +router.post("/", createMovie); +router.get("/:id", findMovieById); +router.put("/:id", updateMovieById); + +module.exports = router; diff --git a/src/routers/screens.js b/src/routers/screens.js new file mode 100644 index 00000000..4ebe216d --- /dev/null +++ b/src/routers/screens.js @@ -0,0 +1,8 @@ +const express = require("express"); +const { createScreen } = require("../controllers/screens"); + +const router = express.Router(); + +router.post("/", createScreen); + +module.exports = router; diff --git a/test/api/routes/customer.spec.js b/test/api/routes/customer.spec.js index c530c8e6..944c87cc 100644 --- a/test/api/routes/customer.spec.js +++ b/test/api/routes/customer.spec.js @@ -1,71 +1,73 @@ -const supertest = require("supertest") -const app = require("../../../src/server.js") -const { createCustomer } = require("../../helpers/createCustomer.js") +const supertest = require("supertest"); +const app = require("../../../src/server.js"); +const { createCustomer } = require("../../helpers/createCustomer.js"); describe("Customer Endpoint", () => { - describe("POST /customers/register", () => { - it("will create a new customer", async () => { - const request = { - name: "john", - phone: "123456", - email: "john@test.com", - } + describe("POST /customers/register", () => { + fit("will create a new customer", async () => { + const request = { + name: "john", + phone: "123456", + email: "john@test.com", + }; - const response = await supertest(app) - .post("/customers/register") - .send(request) + const response = await supertest(app) + .post("/customers/register") + .send(request); - expect(response.status).toEqual(201) - expect(response.body.customer).not.toEqual(undefined) - expect(response.body.customer.id).not.toEqual(undefined) - expect(response.body.customer.name).toEqual(request.name) - expect(response.body.customer.contact.phone).toEqual(request.phone) - expect(response.body.customer.contact.email).toEqual(request.email) - }) + expect(response.status).toEqual(201); + expect(response.body.customer).not.toEqual(undefined); + expect(response.body.customer.id).not.toEqual(undefined); + expect(response.body.customer.name).toEqual(request.name); + expect(response.body.customer.contact.phone).toEqual(request.phone); + expect(response.body.customer.contact.email).toEqual(request.email); + }); - it("will return 400 if one of the required fields is missing", async () => { - const response = await supertest(app).post("/customers/register").send({}) + it("will return 400 if one of the required fields is missing", async () => { + const response = await supertest(app) + .post("/customers/register") + .send({}); - expect(response.status).toEqual(400) - expect(response.body).toHaveProperty('error') - }) + expect(response.status).toEqual(400); + expect(response.body).toHaveProperty("error"); + }); - it("will return 409 when attemping to register a customer with an in-use email address", async () => { - const request = { - name: "john", - phone: "123456", - email: "john@test.com", - } + it("will return 409 when attemping to register a customer with an in-use email address", async () => { + const request = { + name: "john", + phone: "123456", + email: "john@test.com", + }; - await createCustomer(request.name, request.phone, request.email) + await createCustomer(request.name, request.phone, request.email); - const response = await supertest(app) - .post("/customers/register") - .send(request) + const response = await supertest(app) + .post("/customers/register") + .send(request); - expect(response.status).toEqual(409) - expect(response.body).toHaveProperty('error') - }) - }) + expect(response.status).toEqual(409); + expect(response.body).toHaveProperty("error"); + }); + }); - describe("PUT /customers/:id", () => { - it("can update a customers name", async () => { - const customer = await createCustomer("John", "123456", "john@test.com") + describe("PUT /customers/:id", () => { + it("can update a customers name", async () => { + const customer = await createCustomer("John", "123456", "john@test.com"); - const request = { - name: "Jane", - } + const request = { + name: "Jane", + }; - const response = await supertest(app) - .put(`/customers/${customer.id}`) - .send(request) + const response = await supertest(app) + .put(`/customers/${customer.id}`) + .send(request); - expect(response.status).toEqual(201) - expect(response.body.customer).not.toEqual(undefined) - expect(response.body.customer.name).toEqual(request.name) - expect(response.body.customer.contact).not.toEqual(undefined) - expect(response.body.customer.contact.phone).toEqual("123456") - expect(response.body.customer.contact.email).toEqual("john@test.com") - }) - }) -}) \ No newline at end of file + expect(response.status).toEqual(201); + expect(response.body.customer).not.toEqual(undefined); + expect(response.body.customer.name).toEqual(request.name); + expect(response.body.customer.contact).not.toEqual(undefined); + expect(response.body.customer.contact.phone).toEqual("123456"); + expect(response.body.customer.contact.email).toEqual("john@test.com"); + }); + }); +}); diff --git a/test/api/routes/movies.spec.js b/test/api/routes/movies.spec.js index 209c4bc3..e25b31c4 100644 --- a/test/api/routes/movies.spec.js +++ b/test/api/routes/movies.spec.js @@ -1,90 +1,88 @@ -const supertest = require("supertest") -const app = require("../../../src/server.js") -const { createMovie } = require("../../helpers/createMovie.js") -const { createScreen } = require("../../helpers/createScreen.js") +const supertest = require("supertest"); +const app = require("../../../src/server.js"); +const { createMovie } = require("../../helpers/createMovie.js"); +const { createScreen } = require("../../helpers/createScreen.js"); describe("Movies Endpoint", () => { - describe("GET /movies", () => { - it("will retrieve a list of movies", async () => { - const screen = await createScreen(1) - await createMovie('Dodgeball', 120, screen) - await createMovie('Scream', 113, screen) + describe("GET /movies", () => { + it("will retrieve a list of movies", async () => { + const screen = await createScreen(1); + await createMovie("Dodgeball", 120, screen); + await createMovie("Scream", 113, screen); - const response = await supertest(app).get('/movies') + const response = await supertest(app).get("/movies"); - expect(response.status).toEqual(200) - expect(response.body.movies).not.toEqual(undefined) - expect(response.body.movies.length).toEqual(2) + expect(response.status).toEqual(200); + expect(response.body.movies).not.toEqual(undefined); + expect(response.body.movies.length).toEqual(2); - const [movie1, movie2] = response.body.movies - expect(movie1.title).toEqual('Dodgeball') - expect(movie1.runtimeMins).toEqual(120) - expect(movie1.screenings).not.toEqual(undefined) - expect(movie1.screenings.length).toEqual(1) + const [movie1, movie2] = response.body.movies; + expect(movie1.title).toEqual("Dodgeball"); + expect(movie1.runtimeMins).toEqual(120); + expect(movie1.screenings).not.toEqual(undefined); + expect(movie1.screenings.length).toEqual(1); - expect(movie2.title).toEqual('Scream') - expect(movie2.runtimeMins).toEqual(113) - expect(movie2.screenings).not.toEqual(undefined) - expect(movie2.screenings.length).toEqual(1) - }) - }) + expect(movie2.title).toEqual("Scream"); + expect(movie2.runtimeMins).toEqual(113); + expect(movie2.screenings).not.toEqual(undefined); + expect(movie2.screenings.length).toEqual(1); + }); + }); - describe("POST /movies", () => { - it("will create a movie", async () => { - const request = { - title: "Top Gun", - runtimeMins: 110 - } + describe("POST /movies", () => { + it("will create a movie", async () => { + const request = { + title: "Top Gun", + runtimeMins: 110, + }; - const response = await supertest(app) - .post("/movies") - .send(request) + const response = await supertest(app).post("/movies").send(request); - expect(response.status).toEqual(201) - expect(response.body.movie).not.toEqual(undefined) - expect(response.body.movie.title).toEqual('Top Gun') - expect(response.body.movie.runtimeMins).toEqual(110) - expect(response.body.movie.screenings).not.toEqual(undefined) - expect(response.body.movie.screenings.length).toEqual(0) - }) - }) + expect(response.status).toEqual(201); + expect(response.body.movie).not.toEqual(undefined); + expect(response.body.movie.title).toEqual("Top Gun"); + expect(response.body.movie.runtimeMins).toEqual(110); + expect(response.body.movie.screenings).not.toEqual(undefined); + expect(response.body.movie.screenings.length).toEqual(0); + }); + }); - describe("GET /movies/:id", () => { - it("will get a movie by id", async () => { - const screen = await createScreen(1) - const created = await createMovie('Dodgeball', 120, screen) + describe("GET /movies/:id", () => { + it("will get a movie by id", async () => { + const screen = await createScreen(1); + const created = await createMovie("Dodgeball", 120, screen); - const response = await supertest(app).get(`/movies/${created.id}`) + const response = await supertest(app).get(`/movies/${created.id}`); - expect(response.status).toEqual(200) - expect(response.body.movie).not.toEqual(undefined) - expect(response.body.movie.title).toEqual('Dodgeball') - expect(response.body.movie.runtimeMins).toEqual(120) - expect(response.body.movie.screenings).not.toEqual(undefined) - expect(response.body.movie.screenings.length).toEqual(1) - }) - }) + expect(response.status).toEqual(200); + expect(response.body.movie).not.toEqual(undefined); + expect(response.body.movie.title).toEqual("Dodgeball"); + expect(response.body.movie.runtimeMins).toEqual(120); + expect(response.body.movie.screenings).not.toEqual(undefined); + expect(response.body.movie.screenings.length).toEqual(1); + }); + }); - describe("PUT /movies/:id", () => { - it("will update a movie by id", async () => { - const screen = await createScreen(1) - const created = await createMovie('Dodgeball', 120, screen) + describe("PUT /movies/:id", () => { + it("will update a movie by id", async () => { + const screen = await createScreen(1); + const created = await createMovie("Dodgeball", 120, screen); - const request = { - title: 'Scream', - runtimeMins: 113 - } + const request = { + title: "Scream", + runtimeMins: 113, + }; - const response = await supertest(app) - .put(`/movies/${created.id}`) - .send(request) + const response = await supertest(app) + .put(`/movies/${created.id}`) + .send(request); - expect(response.status).toEqual(201) - expect(response.body.movie).not.toEqual(undefined) - expect(response.body.movie.title).toEqual('Scream') - expect(response.body.movie.runtimeMins).toEqual(113) - expect(response.body.movie.screenings).not.toEqual(undefined) - expect(response.body.movie.screenings.length).toEqual(1) - }) - }) -}) + expect(response.status).toEqual(201); + expect(response.body.movie).not.toEqual(undefined); + expect(response.body.movie.title).toEqual("Scream"); + expect(response.body.movie.runtimeMins).toEqual(113); + expect(response.body.movie.screenings).not.toEqual(undefined); + expect(response.body.movie.screenings.length).toEqual(1); + }); + }); +}); diff --git a/test/setupTests.js b/test/setupTests.js index 2be95321..b417c462 100644 --- a/test/setupTests.js +++ b/test/setupTests.js @@ -1,4 +1,4 @@ -const prisma = require("../src/utils/prisma") +const prisma = require("../src/utils/prisma"); const deleteTables = () => { const deleteTables = [ @@ -11,18 +11,18 @@ const deleteTables = () => { ]; // Conditionally delete this table as this will only exist if "Extensions to the Extensions" bullet 2 is implemented - prisma.reviews && deleteTables.push(prisma.reviews.deleteMany()) - return prisma.$transaction(deleteTables) -} + prisma.reviews && deleteTables.push(prisma.reviews.deleteMany()); + return prisma.$transaction(deleteTables); +}; global.beforeAll(() => { - return deleteTables() -}) + return deleteTables(); +}); global.afterEach(() => { - return deleteTables() -}) + return deleteTables(); +}); global.afterAll(() => { - return prisma.$disconnect() -}) + return prisma.$disconnect(); +});