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/package-lock.json b/package-lock.json index 0fc727e5..819a97e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,10 @@ "dependencies": { "@prisma/client": "^5.16.1", "cors": "^2.8.5", + "date-fns": "^3.6.0", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-async-errors": "^3.1.1", "morgan": "^1.10.0" }, "devDependencies": { @@ -1849,6 +1851,15 @@ "node": ">= 8" } }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2146,6 +2157,14 @@ "node": ">= 0.10.0" } }, + "node_modules/express-async-errors": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/express-async-errors/-/express-async-errors-3.1.1.tgz", + "integrity": "sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==", + "peerDependencies": { + "express": "^4.16.2" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", diff --git a/package.json b/package.json index ebaf03be..1ecc824e 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,10 @@ "dependencies": { "@prisma/client": "^5.16.1", "cors": "^2.8.5", + "date-fns": "^3.6.0", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-async-errors": "^3.1.1", "morgan": "^1.10.0" } } diff --git a/prisma/migrations/20240702135814_make_unique/migration.sql b/prisma/migrations/20240702135814_make_unique/migration.sql new file mode 100644 index 00000000..f8077248 --- /dev/null +++ b/prisma/migrations/20240702135814_make_unique/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - A unique constraint covering the columns `[title]` on the table `Movie` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[number]` on the table `Screen` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "Movie_title_key" ON "Movie"("title"); + +-- CreateIndex +CREATE UNIQUE INDEX "Screen_number_key" ON "Screen"("number"); diff --git a/prisma/migrations/20240703083212_reviews/migration.sql b/prisma/migrations/20240703083212_reviews/migration.sql new file mode 100644 index 00000000..3ad22f96 --- /dev/null +++ b/prisma/migrations/20240703083212_reviews/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "Review" ( + "id" SERIAL NOT NULL, + "content" TEXT NOT NULL, + "customerId" INTEGER NOT NULL, + "movieId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Review_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Review" ADD CONSTRAINT "Review_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Review" ADD CONSTRAINT "Review_movieId_fkey" FOREIGN KEY ("movieId") REFERENCES "Movie"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dd9b27f1..e87433e1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -16,6 +16,7 @@ model Customer { name String contact Contact? tickets Ticket[] + reviews Review[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } @@ -33,18 +34,19 @@ model Contact { model Movie { id Int @id @default(autoincrement()) screenings Screening[] - title String + title String @unique runtimeMins Int + reviews Review[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Screen { - id Int @id @default(autoincrement()) - number Int + id Int @id @default(autoincrement()) + number Int @unique screenings Screening[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model Screening { @@ -68,3 +70,14 @@ model Ticket { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } + +model Review { + id Int @id @default(autoincrement()) + content String + customer Customer @relation(fields: [customerId], references: [id]) + customerId Int + movie Movie @relation(fields: [movieId], references: [id]) + movieId Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/prisma/seed.js b/prisma/seed.js index 31e28bfa..03483ca9 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -2,16 +2,19 @@ const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); async function seed() { - await createCustomer(); + const customers = await createCustomer(); const movies = await createMovies(); const screens = await createScreens(); await createScreenings(screens, movies); + await createReviews(customers, movies) process.exit(0); } async function createCustomer() { - const customer = await prisma.customer.create({ + const customers = []; + + const customer1 = await prisma.customer.create({ data: { name: 'Alice', contact: { @@ -26,9 +29,33 @@ async function createCustomer() { } }); - console.log('Customer created', customer); + const customer2 = await prisma.customer.create({ + data: { + name: 'John', + contact: { + create: { + email: 'john@boolean.co.uk', + phone: '1233687890' + } + } + }, + include: { + contact: true + } + }); + + const rawCustomers = [ + customer1, + customer2 + ] + + for (const rawCustomer of rawCustomers) { + customers.push(rawCustomer); + } + + console.log('Customer created', customers); - return customer; + return customers; } async function createMovies() { @@ -97,6 +124,31 @@ async function createScreenings(screens, movies) { } } +async function createReviews(customers, movies) { + for (const customer of customers) { + for (let i = 0; i < movies.length; i++) { + + const review = await prisma.review.create({ + data: { + content: "best movie ever", + movie: { + connect: { + id: movies[i].id + } + }, + customer: { + connect: { + id: customer.id + } + } + } + }); + + console.log('Review created', review); + } + } +} + seed() .catch(async e => { console.error(e); diff --git a/src/controllers/customer.js b/src/controllers/customer.js index 775cfb42..2a9f7631 100644 --- a/src/controllers/customer.js +++ b/src/controllers/customer.js @@ -1,5 +1,8 @@ const { PrismaClientKnownRequestError } = require("@prisma/client") -const { createCustomerDb } = require('../domains/customer.js') +const { createCustomerDb, updateCustomerDb } = require('../domains/customer.js') +const MissingFieldsError = require("../errors/missingFieldsError.js") +const NotFoundError = require("../errors/notFoundError.js") +const NotUniqueError = require("../errors/notUniqueError.js") const createCustomer = async (req, res) => { const { @@ -9,40 +12,46 @@ const createCustomer = async (req, res) => { } = req.body if (!name || !phone || !email) { - return res.status(400).json({ - error: "Missing fields in request body" - }) + throw new MissingFieldsError('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) 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" }) + throw new NotUniqueError("A customer with the provided email already exists" ) } } + } +} + +async function updateCustomer(req, res) { + const customerId = Number(req.params.id) + const { name, contact } = req.body + + if (!name) { + throw new MissingFieldsError('Missing fields in request body') + } + + try { + const updatedCustomer = await updateCustomerDb(customerId, name, contact) - res.status(500).json({ error: e.message }) + res.status(201).json({ + customer: updatedCustomer + }) + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === "P2025") { + throw new NotFoundError('Customer with that id does not exist') + } + } } } module.exports = { - createCustomer + createCustomer, + updateCustomer } diff --git a/src/controllers/movie.js b/src/controllers/movie.js new file mode 100644 index 00000000..e1a7a8a6 --- /dev/null +++ b/src/controllers/movie.js @@ -0,0 +1,87 @@ +const { PrismaClientKnownRequestError } = require("@prisma/client/runtime/library") +const { getAllMoviesDb, createMovieDb, getMovieByIdDb, updateMovieDb } = require("../domains/movie") +const MissingFieldsError = require("../errors/missingFieldsError") +const NotFoundError = require("../errors/notFoundError") +const NotUniqueError = require("../errors/notUniqueError") + +async function getAllMovies(req, res) { + const runtimeLt = Number(req.query.runtimeLt) + const runtimeGt = Number(req.query.runtimeGt) + + const movies = await getAllMoviesDb(runtimeLt, runtimeGt) + + res.json({ + movies + }) +} + +async function createMovie(req, res) { + const { title, runtimeMins, screenings } = req.body + + if (!title || !runtimeMins) { + throw new MissingFieldsError('Missing fields in request body') + } + + try { + const createdMovie = await createMovieDb(title, runtimeMins, screenings) + + res.status(201).json({ + movie: createdMovie + }) + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === "P2002") { + throw new NotUniqueError('A movie with the provided title already exists') + } + } + } +} + +async function getMovieByIdOrTitle(req, res) { + const movieId = req.params.id + + try { + const movie = await getMovieByIdDb(movieId) + + res.json({ + movie + }) + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === "P2025") { + throw new NotFoundError('Movie with that id or title does not exist') + } + } + } + +} + +async function updateMovie(req, res) { + const movieId = Number(req.params.id) + const { title, runtimeMins, screenings } = req.body + + if (!title || !runtimeMins) { + throw new MissingFieldsError('Missing fields in request body') + } + + try { + const updatedMovie = await updateMovieDb(movieId, title, runtimeMins, screenings) + + res.status(201).json({ + movie: updatedMovie + }) + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === "P2025") { + throw new NotFoundError('Movie with that id or title does not exist') + } + } + } +} + +module.exports = { + getAllMovies, + createMovie, + getMovieByIdOrTitle, + updateMovie +} \ No newline at end of file diff --git a/src/controllers/review.js b/src/controllers/review.js new file mode 100644 index 00000000..63fbe104 --- /dev/null +++ b/src/controllers/review.js @@ -0,0 +1,20 @@ +const MissingFieldsError = require("../errors/missingFieldsError") +const { createReviewDb } = require("../domains/review") + +async function createReview(req, res) { + const { content, customerId, movieId } = req.body + + if (!content || !customerId || !movieId) { + throw new MissingFieldsError('Missing fields in request body') + } + + const createdReview = await createReviewDb(content, customerId, movieId) + + res.status(201).json({ + review: createdReview + }) +} + +module.exports = { + createReview +} \ No newline at end of file diff --git a/src/controllers/screen.js b/src/controllers/screen.js new file mode 100644 index 00000000..7eb8496e --- /dev/null +++ b/src/controllers/screen.js @@ -0,0 +1,30 @@ +const { PrismaClientKnownRequestError } = require("@prisma/client/runtime/library") +const { createScreenDb } = require("../domains/screen") +const MissingFieldsError = require("../errors/missingFieldsError") +const NotUniqueError = require("../errors/notUniqueError") + +async function createScreen(req, res) { + const { number, screenings } = req.body + + if (!number) { + throw new MissingFieldsError('Missing fields in request body') + } + + try { + const createdScreen = await createScreenDb(number, screenings) + + res.status(201).json({ + screen: createdScreen + }) + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === "P2002") { + throw new NotUniqueError('A screen with the provided number already exists') + } + } + } +} + +module.exports = { + createScreen +} \ No newline at end of file diff --git a/src/controllers/ticket.js b/src/controllers/ticket.js new file mode 100644 index 00000000..de5176f4 --- /dev/null +++ b/src/controllers/ticket.js @@ -0,0 +1,20 @@ +const { createTicketDb } = require("../domains/ticket") +const MissingFieldsError = require("../errors/missingFieldsError") + +async function createTicket(req, res) { + const { screeningId, customerId } = req.body + + if (!screeningId || !customerId) { + throw new MissingFieldsError('Missing fields in request body') + } + + const createdTicket = await createTicketDb(screeningId, customerId) + + res.status(201).json({ + ticket: createdTicket + }) +} + +module.exports = { + createTicket +} \ No newline at end of file diff --git a/src/domains/customer.js b/src/domains/customer.js index c7f315fd..6fc58d40 100644 --- a/src/domains/customer.js +++ b/src/domains/customer.js @@ -1,9 +1,5 @@ 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, @@ -14,13 +10,37 @@ const createCustomerDb = async (name, phone, email) => await prisma.customer.cre } } }, - // 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 } }) +async function updateCustomerDb(customerId, name, contact) { + const customerData = { + where: { + id: customerId + }, + data: { + name: name + }, + include: { + contact: true + } + } + + if(contact) { + customerData.data.contact = { + update: { + phone: contact.phone, + email: contact.email + } + } + } + + return await prisma.customer.update(customerData) +} + module.exports = { - createCustomerDb + createCustomerDb, + updateCustomerDb } diff --git a/src/domains/movie.js b/src/domains/movie.js new file mode 100644 index 00000000..ace959e6 --- /dev/null +++ b/src/domains/movie.js @@ -0,0 +1,127 @@ +const prisma = require('../utils/prisma') + +async function getAllMoviesDb(runtimeLt, runtimeGt) { + let movieData = null + const currentDate = new Date() + + if (runtimeLt || runtimeGt) { + movieData = { + where: { + OR: [ + { + ...(runtimeLt ? { + runtimeMins: { + lt: runtimeLt + } + } : {})}, + { + ...(runtimeGt ? { + runtimeMins: { + gt: runtimeGt + } + } : {})} + ] + }, + include: { + screenings: true + } + }} else { + movieData = { + where: { + screenings: { + some: { + startsAt: { + gt: currentDate + } + } + } + }, + include: { + screenings: true + } + } + } + + return prisma.movie.findMany(movieData) +} + +async function createMovieDb(title, runtimeMins, screenings) { + const movieData = { + data: { + title: title, + runtimeMins: runtimeMins + }, include: { + screenings: true + } + } + + if(screenings) { + movieData.data.screenings = { + createMany: { + data: screenings.map((screening) => ({ + startsAt: screening.startsAt, + screenId: screening.screenId + })) + } + } + } + + return await prisma.movie.create(movieData) +} + +async function getMovieByIdDb (movieId) { + if (!isNaN(movieId)) { + return await prisma.movie.findUniqueOrThrow({ + where: { + id: Number(movieId) + }, + include: { + screenings: true + } + }) + } + + return await prisma.movie.findUniqueOrThrow({ + where: { + title: movieId + }, + include: { + screenings: true + } + }) +} + +async function updateMovieDb(movieId, title, runtimeMins, screenings) { + const movieData = { + where: { + id: movieId + }, data: { + title: title, + runtimeMins: runtimeMins + }, + include: { + screenings: true + } + } + + if(screenings) { + movieData.data.screenings = { + deleteMany: {}, + createMany: { + data: screenings.map((screening) => ({ + startsAt: screening.startsAt, + screenId: screening.screenId + })) + } + } + } + + return await prisma.movie.update(movieData) +} + +module.exports = { +getAllMoviesDb, +createMovieDb, +getMovieByIdDb, +updateMovieDb +} \ No newline at end of file diff --git a/src/domains/review.js b/src/domains/review.js new file mode 100644 index 00000000..05ed8bf8 --- /dev/null +++ b/src/domains/review.js @@ -0,0 +1,17 @@ +const prisma = require('../utils/prisma') + +async function createReviewDb(content, customerId, movieId) { + const reviewData = { + data: { + content: content, + customerId: customerId, + movieId: movieId + }, + } + + return await prisma.review.create(reviewData) +} + +module.exports = { +createReviewDb +} \ No newline at end of file diff --git a/src/domains/screen.js b/src/domains/screen.js new file mode 100644 index 00000000..1ae14437 --- /dev/null +++ b/src/domains/screen.js @@ -0,0 +1,29 @@ +const prisma = require('../utils/prisma') + +async function createScreenDb(number, screenings) { + const screenData = { + data: { + number: number + }, + include: { + screenings: true + } + } + + if(screenings) { + screenData.data.screenings = { + createMany: { + data: screenings.map((screening) => ({ + startsAt: screening.startsAt, + movieId: screening.movieId + })) + } + } + } + + return await prisma.screen.create(screenData) +} + +module.exports = { +createScreenDb +} \ No newline at end of file diff --git a/src/domains/ticket.js b/src/domains/ticket.js new file mode 100644 index 00000000..99a0be83 --- /dev/null +++ b/src/domains/ticket.js @@ -0,0 +1,30 @@ +const prisma = require('../utils/prisma') + +async function createTicketDb(screeningId, customerId) { + const ticketData = { + data: { + screeningId: screeningId, + customerId: customerId + }, include: { + screening: true, + customer: true, + customer: { + include: { + contact: true + } + }, + screening: { + include: { + screen: true, + movie: true + } + } + } + } + + return await prisma.ticket.create(ticketData) +} + +module.exports = { + createTicketDb +} \ No newline at end of file diff --git a/src/errors/missingFieldsError.js b/src/errors/missingFieldsError.js new file mode 100644 index 00000000..7359cc1a --- /dev/null +++ b/src/errors/missingFieldsError.js @@ -0,0 +1,5 @@ +class MissingFieldsError extends Error { + +} + +module.exports = MissingFieldsError \ No newline at end of file diff --git a/src/errors/notFoundError.js b/src/errors/notFoundError.js new file mode 100644 index 00000000..0a1f7abc --- /dev/null +++ b/src/errors/notFoundError.js @@ -0,0 +1,5 @@ +class NotFoundError extends Error { + +} + +module.exports = NotFoundError \ No newline at end of file diff --git a/src/errors/notUniqueError.js b/src/errors/notUniqueError.js new file mode 100644 index 00000000..a96e8f02 --- /dev/null +++ b/src/errors/notUniqueError.js @@ -0,0 +1,5 @@ +class NotUniqueError extends Error { + +} + +module.exports = NotUniqueError \ No newline at end of file diff --git a/src/routers/customer.js b/src/routers/customer.js index f14a87fc..47df89b1 100644 --- a/src/routers/customer.js +++ b/src/routers/customer.js @@ -1,13 +1,13 @@ const express = require("express"); const { - createCustomer + createCustomer, + updateCustomer } = require('../controllers/customer'); const router = express.Router(); -// In index.js, we told express that the /customer route should use this router file -// The below /register route extends that, so the end result will be a URL -// that looks like http://localhost:4040/customer/register router.post("/register", createCustomer); +router.put('/:id', updateCustomer) + module.exports = router; diff --git a/src/routers/movie.js b/src/routers/movie.js new file mode 100644 index 00000000..f05e6db4 --- /dev/null +++ b/src/routers/movie.js @@ -0,0 +1,11 @@ +const express = require("express"); +const { getAllMovies, createMovie, updateMovie, getMovieByIdOrTitle } = require("../controllers/movie"); + +const router = express.Router(); + +router.get('/', getAllMovies) +router.post('/', createMovie) +router.get('/:id', getMovieByIdOrTitle) +router.put('/:id', updateMovie) + +module.exports = router; \ No newline at end of file diff --git a/src/routers/review.js b/src/routers/review.js new file mode 100644 index 00000000..2890ce58 --- /dev/null +++ b/src/routers/review.js @@ -0,0 +1,8 @@ +const express = require("express"); +const { createReview } = require("../controllers/review"); + +const router = express.Router(); + +router.post('/', createReview) + +module.exports = router; \ No newline at end of file diff --git a/src/routers/screen.js b/src/routers/screen.js new file mode 100644 index 00000000..60704ae4 --- /dev/null +++ b/src/routers/screen.js @@ -0,0 +1,8 @@ +const express = require("express"); +const { createScreen } = require("../controllers/screen"); + +const router = express.Router(); + +router.post('/', createScreen) + +module.exports = router; \ No newline at end of file diff --git a/src/routers/ticket.js b/src/routers/ticket.js new file mode 100644 index 00000000..25e9350a --- /dev/null +++ b/src/routers/ticket.js @@ -0,0 +1,8 @@ +const express = require("express"); +const { createTicket } = require("../controllers/ticket"); + +const router = express.Router(); + +router.post('/', createTicket) + +module.exports = router; \ No newline at end of file diff --git a/src/server.js b/src/server.js index 93d47a16..5d62377a 100644 --- a/src/server.js +++ b/src/server.js @@ -1,21 +1,58 @@ -const express = require('express'); -const app = express(); +const express = require('express') +require('express-async-errors') +const app = express() -const cors = require('cors'); -const morgan = require('morgan'); +const cors = require('cors') +const morgan = require('morgan') -app.disable('x-powered-by'); +app.disable('x-powered-by') -// Add middleware -app.use(cors()); -app.use(morgan('dev')); -app.use(express.json()); -app.use(express.urlencoded({ extended: true })); +app.use(cors()) +app.use(morgan('dev')) +app.use(express.json()) +app.use(express.urlencoded({ extended: true })) +const customerRouter = require('./routers/customer.js') +app.use('/customers', customerRouter) -// Tell express to use your routers here -const customerRouter = require('./routers/customer'); -app.use('/customers', customerRouter); +const movieRouter = require('./routers/movie.js') +app.use('/movies', movieRouter) +const screenRouter = require('./routers/screen.js') +app.use('/screens', screenRouter) + +const ticketRouter = require('./routers/ticket.js') +app.use('/tickets', ticketRouter) + +const reviewRouter = require('./routers/review.js') +app.use('/reviews', reviewRouter) + +const MissingFieldsError = require('./errors/missingFieldsError.js') +const NotFoundError = require('./errors/notFoundError.js') +const NotUniqueError = require('./errors/notUniqueError.js') + +app.use((error, req, res, next) => { + if (error instanceof MissingFieldsError) { + return res.status(400).json({ + error: error.message + }) + } + + if (error instanceof NotFoundError) { + return res.status(404).json({ + error: error.message + }) + } + + if (error instanceof NotUniqueError) { + return res.status(409).json({ + error: error.message + }) + } + + res.status(500).json({ + error: error.message + }) +}) module.exports = app diff --git a/test/api/extensions/customer-ext.spec.js b/test/api/extensions/customer-ext.spec.js index fee884bb..9c977439 100644 --- a/test/api/extensions/customer-ext.spec.js +++ b/test/api/extensions/customer-ext.spec.js @@ -53,4 +53,4 @@ describe("Customer Endpoint", () => { expect(response.body).toHaveProperty('error') }) }) -}) +}) \ No newline at end of file diff --git a/test/api/extensions/movie-ext.spec.js b/test/api/extensions/movie-ext.spec.js new file mode 100644 index 00000000..8bbe8982 --- /dev/null +++ b/test/api/extensions/movie-ext.spec.js @@ -0,0 +1,215 @@ +const supertest = require("supertest") +const app = require("../../../src/server.js") +const { createMovie } = require("../../helpers/createMovie") +const { createScreen } = require("../../helpers/createScreen") + +describe("Movie Endpoint", () => { + describe("GET /movies?runtimeLt", () => { + it("will return movies with a runtime less than runtimeLt", async () => { + await createMovie("test1", 130) + await createMovie("test2", 135) + await createMovie('test3', 150) + + const response = await supertest(app) + .get('/movies?runtimeLt=140') + + 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('test1') + expect(movie1.runtimeMins).toEqual(130) + + expect(movie2.title).toEqual('test2') + expect(movie2.runtimeMins).toEqual(135) + }) + }) + + describe("GET /movies?runtimeGt", () => { + it("will return movies with a runtime more than runtimeGt", async () => { + await createMovie("test1", 130) + await createMovie("test2", 135) + await createMovie('test3', 150) + + const response = await supertest(app) + .get('/movies?runtimeGt=134') + + 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('test2') + expect(movie1.runtimeMins).toEqual(135) + + expect(movie2.title).toEqual('test3') + expect(movie2.runtimeMins).toEqual(150) + }) + }) + + describe("GET /movies?runtimeLt&runtimeGt", () => { + it("will return movies with a runtime less than runtimeLt and more than runtimeGt", async () => { + await createMovie("test1", 130) + await createMovie("test2", 140) + await createMovie('test3', 150) + + const response = await supertest(app) + .get('/movies?runtimeLt=135&runtimeGt=145') + + 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('test1') + expect(movie1.runtimeMins).toEqual(130) + + expect(movie2.title).toEqual('test3') + expect(movie2.runtimeMins).toEqual(150) + }) + }) + + describe("POST /movies", () => { + it("will add a screening if the property exists in the body", async () => { + const screen1 = await createScreen(1) + const screen2 = await createScreen(2) + + const request = { + title: "Minions", + runtimeMins: 120, + screenings: [ + { + movieId: 1, + screenId: screen1.id, + startsAt: "2022-06-11T18:30:00.000Z" + }, + { + movieId: 1, + screenId: screen2.id, + startsAt: "2023-06-11T18:30:00.000Z" + } + ] + } + + 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('Minions') + expect(response.body.movie.runtimeMins).toEqual(120) + expect(response.body.movie.screenings).not.toEqual(undefined) + expect(response.body.movie.screenings.length).toEqual(2) + }) + + it("will return 400 when there are missing fields in the request body", async () => { + const request = {} + + const response = await supertest(app) + .post('/movies') + .send(request) + + expect(response.status).toEqual(400) + expect(response.body).toHaveProperty('error') + }) + + it("will return 409 when a movie with the provided title already exists", async () => { + const screen = await createScreen(1) + const movie = await createMovie("test1", 130, screen) + + const request = { + title: movie.title, + runtimeMins: movie.runtimeMins + } + + const response = await supertest(app) + .post('/movies') + .send(request) + + expect(response.status).toEqual(409) + expect(response.body).toHaveProperty('error') + }) + }) + + describe("GET /movies/title", () => { + it("will get movies by title", async () => { + const screen = await createScreen(1) + const movie = await createMovie('Dodgeball', 120, screen) + + const response = await supertest(app).get(`/movies/${movie.title}`) + + 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) + }) + + it('will return 404 if the movie is not found by id', async () => { + const response = await supertest(app) + .get('/movies/10000') + + expect(response.status).toEqual(404) + expect(response.body).toHaveProperty('error') + }) + + it('will return 404 if the movie is not found by title', async () => { + const response = await supertest(app) + .get('/movies/bkdscjcd') + + expect(response.status).toEqual(404) + expect(response.body).toHaveProperty('error') + }) + }) + + describe("PUT /movies/:id", () => { + it("will update a movie by id and the screenings when provided", async () => { + const screen1 = await createScreen(1) + const screen2 = await createScreen(2) + const created = await createMovie('Dodgeball', 120, screen1) + + const request = { + title: 'Scream', + runtimeMins: 113, + screenings: [ + { + screenId: screen1.id, + startsAt: "2022-06-11T18:30:00.000Z" + }, + { + screenId: screen2.id, + startsAt: "2023-06-11T18:30:00.000Z" + } + ] + } + + 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(2) + }) + + it("will return 400 when there are missing fields in the request body", async () => { + const screen = await createScreen(1) + const movie = await createMovie('Dodgeball', 120, screen) + + const request = {} + + const response = await supertest(app) + .put(`/movies/${movie.id}`) + .send(request) + + expect(response.status).toEqual(400) + expect(response.body).toHaveProperty('error') + }) + }) +}) diff --git a/test/api/extensions/review-ext.spec.js b/test/api/extensions/review-ext.spec.js new file mode 100644 index 00000000..3e6a5a1e --- /dev/null +++ b/test/api/extensions/review-ext.spec.js @@ -0,0 +1,29 @@ +const supertest = require("supertest") +const app = require("../../../src/server.js") +const { createMovie } = require("../../helpers/createMovie.js") +const { createScreen } = require("../../helpers/createScreen.js") +const { createCustomer } = require("../../helpers/createCustomer.js") + +describe("Reviews Endpoint", () => { + describe("POST /reviews", () => { + it("will create a review", async () => { + const screen = await createScreen(1) + const movie = await createMovie("Minions", 150, screen) + const customer = await createCustomer("John", "123456", "john@test.com") + + const request = { + content: "Worst movie ever", + movieId: movie.id, + customerId: customer.id + } + + const response = await supertest(app) + .post("/reviews") + .send(request) + + expect(response.status).toEqual(201) + expect(response.body.review).not.toEqual(undefined) + expect(response.body.review.content).toEqual('Worst movie ever') + }) + }) +}) diff --git a/test/api/extensions/screen-ext.spec.js b/test/api/extensions/screen-ext.spec.js new file mode 100644 index 00000000..3cac262e --- /dev/null +++ b/test/api/extensions/screen-ext.spec.js @@ -0,0 +1,63 @@ +const supertest = require("supertest") +const app = require("../../../src/server.js") +const { createMovie } = require("../../helpers/createMovie") +const { createScreen } = require("../../helpers/createScreen.js") + +describe("Screen Endpoint", () => { + describe("POST /screens", () => { + it("will create screenings for a movie if the request body has a screenings property", async () => { + const movie1 = await createMovie("test1", 130) + const movie2 = await createMovie("test2", 150) + + const request = { + number: 10, + screenings: [ + { + movieId: movie1.id, + startsAt: "2022-06-11T18:30:00.000Z" + }, + { + movieId: movie2.id, + startsAt: "2023-06-11T18:30:00.000Z" + } + ] + } + + const response = await supertest(app) + .post("/screens") + .send(request) + + expect(response.status).toEqual(201) + expect(response.body.screen).not.toEqual(undefined) + expect(response.body.screen.number).toEqual(10) + expect(response.body.screen.screenings).not.toEqual(undefined) + expect(response.body.screen.screenings.length).toEqual(2) + }) + + it("will return 400 when there are missing fields in the request body", async () => { + const request = {} + + const response = await supertest(app) + .post('/movies') + .send(request) + + expect(response.status).toEqual(400) + expect(response.body).toHaveProperty('error') + }) + + it("will return 409 when a screen with the provided number already exists", async () => { + const screen = await createScreen(1) + + const request = { + number: screen.number + } + + const response = await supertest(app) + .post('/screens') + .send(request) + + expect(response.status).toEqual(409) + expect(response.body).toHaveProperty('error') + }) + }) +}) \ No newline at end of file diff --git a/test/api/extensions/ticket-ext.spec.js b/test/api/extensions/ticket-ext.spec.js new file mode 100644 index 00000000..defb2f72 --- /dev/null +++ b/test/api/extensions/ticket-ext.spec.js @@ -0,0 +1,48 @@ +const supertest = require("supertest") +const app = require("../../../src/server.js") +const { createCustomer } = require("../../helpers/createCustomer.js") +const { createScreen } = require("../../helpers/createScreen") +const { createMovie } = require("../../helpers/createMovie") + + +describe("Ticket Endpoint", () => { + describe("POST /tickets", () => { + it("can create a ticket", async () => { + const customer = await createCustomer("John", "123456", "john@test.com") + const screen = await createScreen(1) + const movie = await createMovie('Dodgeball', 120, screen) + + + const request = { + screeningId: movie.screenings[0].id, + customerId: customer.id + } + + const response = await supertest(app) + .post('/tickets') + .send(request) + + expect(response.status).toEqual(201) + expect(response.body.ticket.customer).not.toEqual(undefined) + expect(response.body.ticket.customer.name).toEqual("John") + expect(response.body.ticket.customer.contact).not.toEqual(undefined) + expect(response.body.ticket.customer.contact.phone).toEqual("123456") + expect(response.body.ticket.customer.contact.email).toEqual("john@test.com") + expect(response.body.ticket.screening).not.toEqual(undefined) + expect(response.body.ticket.screening.screen.number).toEqual(1) + expect(response.body.ticket.screening.movie.title).toEqual("Dodgeball") + expect(response.body.ticket.screening.movie.runtimeMins).toEqual(120) + }) + + it("will return 400 when there are missing fields in the request body", async () => { + const request = {} + + const response = await supertest(app) + .post('/tickets') + .send(request) + + expect(response.status).toEqual(400) + expect(response.body).toHaveProperty('error') + }) + }) +}) \ No newline at end of file diff --git a/test/helpers/createMovie.js b/test/helpers/createMovie.js index 06903fea..066a80a0 100644 --- a/test/helpers/createMovie.js +++ b/test/helpers/createMovie.js @@ -1,6 +1,8 @@ const prisma = require("../../src/utils/prisma") +const { addDays } = require("date-fns") const createMovie = async (title, runtimeMins, screen = null) => { + const date = addDays(new Date(), 10) const movieData = { data: { title: title, @@ -15,7 +17,7 @@ const createMovie = async (title, runtimeMins, screen = null) => { movieData.data.screenings = { create: [ { - startsAt: "2022-06-11T18:30:00.000Z", + startsAt: date, screenId: screen.id } ] diff --git a/test/setupTests.js b/test/setupTests.js index 2be95321..0edacb87 100644 --- a/test/setupTests.js +++ b/test/setupTests.js @@ -4,6 +4,7 @@ const deleteTables = () => { const deleteTables = [ prisma.ticket.deleteMany(), prisma.screening.deleteMany(), + prisma.review.deleteMany(), prisma.movie.deleteMany(), prisma.screen.deleteMany(), prisma.contact.deleteMany(),