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..dcdde761 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-async-errors": "^3.1.1", "morgan": "^1.10.0" }, "devDependencies": { @@ -1433,12 +1434,12 @@ } }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -1446,7 +1447,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -1493,12 +1494,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1784,9 +1785,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -2106,16 +2107,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -2146,6 +2147,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", @@ -2195,9 +2204,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -4134,9 +4143,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", diff --git a/package.json b/package.json index ebaf03be..35df3710 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-async-errors": "^3.1.1", "morgan": "^1.10.0" } } diff --git a/prisma/migrations/20240704151358_add_reviews/migration.sql b/prisma/migrations/20240704151358_add_reviews/migration.sql new file mode 100644 index 00000000..451da56a --- /dev/null +++ b/prisma/migrations/20240704151358_add_reviews/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "Review" ( + "id" SERIAL NOT NULL, + "customerId" INTEGER NOT NULL, + "movieId" INTEGER NOT NULL, + "content" VARCHAR(255) 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/migrations/20240704172759_reviews_corrections/migration.sql b/prisma/migrations/20240704172759_reviews_corrections/migration.sql new file mode 100644 index 00000000..af5102c8 --- /dev/null +++ b/prisma/migrations/20240704172759_reviews_corrections/migration.sql @@ -0,0 +1 @@ +-- This is an empty migration. \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dd9b27f1..df2d22ee 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,22 +12,23 @@ 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 + review Review[] } 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 { @@ -37,34 +38,46 @@ model Movie { runtimeMins Int createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + review Review[] } 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 +} + +model Ticket { 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 + 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 } -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 +model Review { + id Int @id @default(autoincrement()) + customer Customer @relation(fields: [customerId], references: [id]) + customerId Int + movie Movie @relation(fields: [movieId], references: [id]) + movieId Int + content String @db.VarChar(255) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } diff --git a/prisma/seed.js b/prisma/seed.js index 31e28bfa..1fb680ee 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -97,6 +97,30 @@ 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: "Lorem ipsum great movie", + movie: { + connect: { + id: movies[i].id, + }, + }, + customer: { + connect: { + id: customer.id, + }, + }, + }, + }) + + console.log("Reviews created", review) + } + } +} + seed() .catch(async e => { console.error(e); diff --git a/src/controllers/customer.js b/src/controllers/customer.js index 775cfb42..cea4391c 100644 --- a/src/controllers/customer.js +++ b/src/controllers/customer.js @@ -1,48 +1,88 @@ const { PrismaClientKnownRequestError } = require("@prisma/client") -const { createCustomerDb } = require('../domains/customer.js') +const { + createCustomerDb, + getAllCustomersDb, + updateCustomerDb, +} = require("../domains/customer.js") + +const { + MissingFieldsError, + ExistingDataError, + DataNotFoundError, +} = require("../errors/errors") const createCustomer = async (req, res) => { - const { - name, - phone, - email - } = req.body - - if (!name || !phone || !email) { - return res.status(400).json({ - 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) - - 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" }) - } - } - - res.status(500).json({ error: e.message }) - } + const { name, phone, email } = req.body + + if (!name || !phone || !email) { + return res.status(400).json({ + error: "Missing fields in request body", + }) + } + + try { + const createdCustomer = await createCustomerDb(name, phone, email) + + res.status(201).json({ customer: createdCustomer }) + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === "P2002") { + return res.status(409).json({ + error: "A customer with the provided email already exists", + }) + } + } + + res.status(500).json({ error: e.message }) + } +} + +const updateCustomer = async (req, res) => { + const reqId = Number(req.params.id) + const { name, contact } = req.body + + const customersList = await getAllCustomersDb() + const existingCustomer = customersList.find( + (cus) => cus.id === reqId + ) + + if (!existingCustomer) { + throw new DataNotFoundError( + "No customer with the provided ID exists" + ) + } + if (!name) { + throw new MissingFieldsError( + "A name must be provided in order to update the customer" + ) + } + const updatedCustomer = await updateCustomerDb(reqId, name, contact) + res.status(201).json({ customer: updatedCustomer }) +} + +const getAllCustomers = async (req, res) => { + const allCustomers = await getAllCustomersDb() + res.status(200).json({ allCustomers }) } module.exports = { - createCustomer + createCustomer, + getAllCustomers, + updateCustomer, } + +// 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. + +// 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 diff --git a/src/controllers/movie.js b/src/controllers/movie.js new file mode 100644 index 00000000..0c54d881 --- /dev/null +++ b/src/controllers/movie.js @@ -0,0 +1,99 @@ +const { PrismaClientKnownRequestError } = require("@prisma/client") +const { + getAllMoviesDb, + getMovieByIdDb, + createMovieDb, + updateMovieDb, +} = require("../domains/movie") + +const { + MissingFieldsError, + ExistingDataError, + DataNotFoundError, +} = require("../errors/errors") + +const getAllMovies = async (req, res) => { + const { runtimeLt, runtimeGt } = req.query + + const allMovies = await getAllMoviesDb(runtimeLt, runtimeGt) + + res.status(200).json({ movies: allMovies }) +} + +const createMovie = async (req, res) => { + const { title, runtimeMins, screenings } = req.body + + if (!title || !runtimeMins) { + throw new MissingFieldsError( + "Title and duration in minutes must be provided in order to add a movie" + ) + } + + const moviesList = await getMovieByIdDb(title) + + if (moviesList) { + throw new ExistingDataError( + "A movie with the provided title already exists" + ) + } + + if (screenings) { + const createdMovie = await createMovieDb( + title, + runtimeMins, + screenings + ) + res.status(201).json({ movie: createdMovie }) + return + } + + const createdMovie = await createMovieDb(title, runtimeMins) + res.status(201).json({ movie: createdMovie }) +} + +const getMovieById = async (req, res) => { + const idOrTitle = req.params.id + + const movie = await getMovieByIdDb(idOrTitle) + if (!movie) { + throw new DataNotFoundError( + "No movie with the provided id or title exists" + ) + } + res.status(200).json({ movie: movie }) +} + +const updateMovie = async (req, res) => { + const reqId = Number(req.params.id) + const updateInfo = req.body + const screenings = updateInfo.screenings + + const moviesList = await getMovieByIdDb(reqId) + + if (!moviesList) { + throw new DataNotFoundError( + "No movie with the provided ID exists" + ) + } + + if (!updateInfo.title || !updateInfo.runtimeMins) { + throw new MissingFieldsError( + "Title and duration in minutes must be provided in order to update a movie" + ) + } + + const updatedMovie = await updateMovieDb( + reqId, + updateInfo.title, + updateInfo.runtimeMins, + screenings + ) + res.status(201).json({ movie: updatedMovie }) +} + +module.exports = { + getAllMovies, + getMovieById, + createMovie, + updateMovie, +} diff --git a/src/controllers/review.js b/src/controllers/review.js new file mode 100644 index 00000000..ad65ce8f --- /dev/null +++ b/src/controllers/review.js @@ -0,0 +1,50 @@ +const { PrismaClientKnownRequestError } = require("@prisma/client") +const { + createReviewDb, + getAllreviewsDb, + getReviewsByMovieTitleDb, +} = require("../domains/review") +const { getMovieByIdDb } = require("../domains/movie") + +const { + MissingFieldsError, + DataNotFoundError, +} = require("../errors/errors") + +const getAllReviews = async (req, res) => { + const allReviews = await getAllreviewsDb() + res.status(200).json({ reviews: allReviews }) +} + +const createReview = async (req, res) => { + const { movieId, customerId, content } = req.body + + if (!movieId || !customerId || !content) { + throw new MissingFieldsError( + "Movie ID, Customer ID and the review text, must be provided in order to add a review" + ) + } + + const review = await createReviewDb(movieId, customerId, content) + res.status(201).json({ review: review }) +} + +const getReviewsByMovieTitle = async (req, res) => { + const { idOrTitle } = req.params + const movie = await getMovieByIdDb(idOrTitle) + + if (!movie) { + throw new DataNotFoundError( + "No movie with the provided title exists" + ) + } + + const movieReviews = await getReviewsByMovieTitleDb(idOrTitle) + res.status(200).json({ reviews: movieReviews }) +} + +module.exports = { + createReview, + getAllReviews, + getReviewsByMovieTitle, +} diff --git a/src/controllers/screen.js b/src/controllers/screen.js new file mode 100644 index 00000000..8f452763 --- /dev/null +++ b/src/controllers/screen.js @@ -0,0 +1,42 @@ +const { PrismaClientKnownRequestError } = require("@prisma/client") +const { + createScreenDB, + getAllScreensDb, +} = require("../domains/screen") +const { + MissingFieldsError, + ExistingDataError, +} = require("../errors/errors") + +const createScreen = async (req, res) => { + const screnToAdd = req.body + + if (!screnToAdd.number) { + throw new MissingFieldsError( + "Number must be provided in order to add a screen" + ) + } + + const screensList = await getAllScreensDb() + const existingScreen = screensList.find( + (scr) => scr.number === screnToAdd.number + ) + + if (existingScreen) { + throw new ExistingDataError( + "A screen with the provided number already exists" + ) + } + const createScreen = await createScreenDB( + screnToAdd.number, + screnToAdd.screenings + ) + res.status(201).json({ screen: createScreen }) +} + +const getAllScreens = async (req, res) => { + const allScreens = await getAllScreensDb() + res.status(200).json({ allScreens }) +} + +module.exports = { createScreen, getAllScreens } diff --git a/src/controllers/screening.js b/src/controllers/screening.js new file mode 100644 index 00000000..783e6a51 --- /dev/null +++ b/src/controllers/screening.js @@ -0,0 +1,9 @@ +const { PrismaClientKnownRequestError } = require("@prisma/client") +const { getAllScreeningsDb } = require("../domains/screening") + +const getAllScreenings = async (req, res) => { + const allScreenings = await getAllScreeningsDb() + res.status(200).json({ allScreenings }) +} + +module.exports = { getAllScreenings } diff --git a/src/controllers/ticket.js b/src/controllers/ticket.js new file mode 100644 index 00000000..a2ca3c45 --- /dev/null +++ b/src/controllers/ticket.js @@ -0,0 +1,47 @@ +const { PrismaClientKnownRequestError } = require("@prisma/client") +const { + createTicketDB, + getAllTicketsDb, +} = require("../domains/ticket") +const { getAllCustomersDb } = require("../domains/customer") +const { getAllScreeningsDb } = require("../domains/screening") + +const { + MissingFieldsError, + DataNotFoundError, +} = require("../errors/errors") +const { response } = require("express") + +const createTicket = async (req, res) => { + const { screeningId, customerId } = req.body + + const customers = await getAllCustomersDb() + const existingCustomer = customers.find( + (cus) => cus.id === customerId + ) + const screenings = await getAllScreeningsDb() + const existingScreening = screenings.find( + (scr) => scr.id === screeningId + ) + + if (!screeningId || !customerId) { + throw new MissingFieldsError( + "Screening and Customer ID must be provided in order to create a ticket" + ) + } + + if (!existingScreening || !existingCustomer) { + throw new DataNotFoundError( + "No customer or screening exist with the provided id" + ) + } + const newTicket = await createTicketDB(screeningId, customerId) + res.status(201).json({ ticket: newTicket }) +} + +const getAllTickets = async (req, res) => { + const alltickets = await getAllTicketsDb() + res.status(200).json({ tickets: alltickets }) +} + +module.exports = { createTicket, getAllTickets } diff --git a/src/domains/customer.js b/src/domains/customer.js index c7f315fd..90c918a0 100644 --- a/src/domains/customer.js +++ b/src/domains/customer.js @@ -1,26 +1,61 @@ -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, + }, + }, + }, + include: { + contact: true, + }, + }) + +const getAllCustomersDb = async () => { + const allCustomers = await prisma.customer.findMany() + return allCustomers +} + +const updateCustomerDb = async (reqId, updateInfo, contactInfo) => { + const updateData = { + name: updateInfo, + } + + if (contactInfo) { + updateData.contact = { + update: { + phone: contactInfo.phone, + email: contactInfo.email, + }, + } + } + + const updatedCustomer = await prisma.customer.update({ + where: { + id: reqId, + }, + data: updateData, + include: { + contact: true, + }, + }) + return updatedCustomer +} module.exports = { - createCustomerDb + createCustomerDb, + getAllCustomersDb, + updateCustomerDb, } + +// 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 diff --git a/src/domains/movie.js b/src/domains/movie.js new file mode 100644 index 00000000..b5e133a6 --- /dev/null +++ b/src/domains/movie.js @@ -0,0 +1,132 @@ +const prisma = require("../utils/prisma") + +const getAllMoviesDb = async (runtimeLt, runtimeGt) => { + const filterRuntimes = {} + const now = new Date() + + + if (runtimeLt) { + filterRuntimes.lt = Number(runtimeLt) + } + if (runtimeGt) { + filterRuntimes.gt = Number(runtimeGt) + } + + const allMovies = await prisma.movie.findMany({ + where: { + runtimeMins: Object.keys(filterRuntimes).length + ? filterRuntimes + : undefined, + screenings: { + some: { + startsAt: { gt: now }, + }, + }, + }, + include: { + screenings: true, + }, + }) + return allMovies +} + +const createMovieDb = async (title, minutes, screenings) => { + if (screenings) { + const newMovie = await prisma.movie.create({ + data: { + title: title, + runtimeMins: minutes, + screenings: { + create: screenings.map((scr) => ({ + startsAt: new Date(scr.startsAt), + screenId: scr.screenId, + })), + }, + }, + include: { + screenings: true, + }, + }) + return newMovie + } else { + const newMovie = await prisma.movie.create({ + data: { + title: title, + runtimeMins: minutes, + }, + include: { + screenings: true, + }, + }) + + return newMovie + } +} + +const getMovieByTitleDb = async (title) => { + const foundMovie = await prisma.movie.findFirst({ + where: { + title: title, + }, + include: { + screenings: true, + }, + }) + return foundMovie +} + +const getMovieByIdDb = async (reqId) => { + const reqType = Number(reqId) + + if (isNaN(reqType)) { + return await getMovieByTitleDb(reqId) + } + + const foundMovie = await prisma.movie.findUnique({ + where: { + id: reqType, + }, + include: { + screenings: true, + }, + }) + return foundMovie +} + +const updateMovieDb = async (reqId, title, minutes, screenings) => { + const updateData = { + title: title, + runtimeMins: minutes, + } + + if (screenings) { + updateData.screenings = { + deleteMany: {}, + create: screenings.map((scr) => ({ + startsAt: new Date(scr.startsAt), + screen: { + connect: { + id: scr.screenId, + }, + }, + })), + } + } + const movieToUpdate = await prisma.movie.update({ + where: { + id: reqId, + }, + data: updateData, + include: { + screenings: true, + }, + }) + return movieToUpdate +} + +module.exports = { + getAllMoviesDb, + createMovieDb, + getMovieByIdDb, + updateMovieDb, +} diff --git a/src/domains/review.js b/src/domains/review.js new file mode 100644 index 00000000..49592fa9 --- /dev/null +++ b/src/domains/review.js @@ -0,0 +1,36 @@ +const prisma = require("../utils/prisma") + +const createReviewDb = async (movieId, customerId, content) => { + const review = await prisma.review.create({ + data: { + movieId: movieId, + customerId: customerId, + content: content, + }, + }) + return review +} + +const getAllreviewsDb = async () => { + const getAllReviews = await prisma.review.findMany({ + include: { + movie: true, + } + }) + return getAllReviews +} + + +const getReviewsByMovieTitleDb = async (title) => { + const getReviewsForMovie = await prisma.review.findFirst({ + where: { + movie: { + title: title, + }, + }, + }) + return getReviewsForMovie +} + + +module.exports = { createReviewDb, getAllreviewsDb, getReviewsByMovieTitleDb } diff --git a/src/domains/screen.js b/src/domains/screen.js new file mode 100644 index 00000000..cc26c045 --- /dev/null +++ b/src/domains/screen.js @@ -0,0 +1,33 @@ +const prisma = require("../utils/prisma") + +const createScreenDB = async (number, screenings) => { + if (screenings) { + const newScreen = await prisma.screen.create({ + data: { + number: number, + screenings: { + create: screenings.map((scr) => ({ + movieId: scr.movieId, + screenId: scr.screenId, + startsAt: new Date(scr.startsAt), + })), + }, + }, + include: { screenings: true }, + }) + return newScreen + } + + const newScreen = await prisma.screen.create({ + data: { number: number }, + include: { screenings: true }, + }) + return newScreen +} + +const getAllScreensDb = async () => { + const allScreens = await prisma.screen.findMany() + return allScreens +} + +module.exports = { createScreenDB, getAllScreensDb } diff --git a/src/domains/screening.js b/src/domains/screening.js new file mode 100644 index 00000000..929c1348 --- /dev/null +++ b/src/domains/screening.js @@ -0,0 +1,10 @@ +const prisma = require("../utils/prisma") + +const getAllScreeningsDb = async () => { + const allScreenings = await prisma.screening.findMany() + return allScreenings +} + +module.exports = { + getAllScreeningsDb +} \ No newline at end of file diff --git a/src/domains/ticket.js b/src/domains/ticket.js new file mode 100644 index 00000000..6bac53d7 --- /dev/null +++ b/src/domains/ticket.js @@ -0,0 +1,46 @@ +const prisma = require("../utils/prisma") + +const createTicketDB = async (screen, customer) => { + const newTicket = await prisma.ticket.create({ + data: { + screening: { + connect: { id: screen }, + }, + customer: { + connect: { id: customer }, + }, + }, + select: { + id: true, + screening: { + select: { + id: true, + movieId: true, + screenId: true, + }, + }, + customer: { + include: { + contact: true, + }, + }, + screening: { + select: { + screen: true, + movie: true, + }, + }, + }, + }) + return newTicket +} + +const getAllTicketsDb = async () => { + const allTickets = await prisma.ticket.findMany() + return allTickets +} + +module.exports = { + createTicketDB, + getAllTicketsDb, +} diff --git a/src/errors/errors.js b/src/errors/errors.js new file mode 100644 index 00000000..d16a9f6b --- /dev/null +++ b/src/errors/errors.js @@ -0,0 +1,14 @@ +class MissingFieldsError extends Error {} + +class ExistingDataError extends Error {} + +class DataNotFoundError extends Error {} + +class IncorrectFieldTypeError extends Error {} + +module.exports = { + MissingFieldsError, + ExistingDataError, + DataNotFoundError, + IncorrectFieldTypeError, +} diff --git a/src/routers/customer.js b/src/routers/customer.js index f14a87fc..47b283ad 100644 --- a/src/routers/customer.js +++ b/src/routers/customer.js @@ -1,6 +1,6 @@ const express = require("express"); const { - createCustomer + createCustomer, getAllCustomers, updateCustomer } = require('../controllers/customer'); const router = express.Router(); @@ -10,4 +10,7 @@ const router = express.Router(); // that looks like http://localhost:4040/customer/register router.post("/register", createCustomer); +router.put('/:id', updateCustomer) + +router.get("/", getAllCustomers) module.exports = router; diff --git a/src/routers/movie.js b/src/routers/movie.js new file mode 100644 index 00000000..4b144832 --- /dev/null +++ b/src/routers/movie.js @@ -0,0 +1,19 @@ +const express = require("express") +const { + getAllMovies, + getMovieById, + updateMovie, + createMovie, +} = require("../controllers/movie") + +const router = express.Router() + +router.get("/", getAllMovies) + +router.post("/", createMovie) + +router.get("/:id", getMovieById) + +router.put("/:id", updateMovie) + +module.exports = router diff --git a/src/routers/review.js b/src/routers/review.js new file mode 100644 index 00000000..7e0c246f --- /dev/null +++ b/src/routers/review.js @@ -0,0 +1,17 @@ +const express = require("express") +const { + createReview, + getAllReviews, + getReviewsByMovieTitle, +} = require("../controllers/review") + +const router = express.Router() + +router.get('/', getAllReviews) + +router.get('/:idOrTitle', getReviewsByMovieTitle) + +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..6eab7b98 --- /dev/null +++ b/src/routers/screen.js @@ -0,0 +1,10 @@ +const express = require("express") +const { createScreen, getAllScreens } = require('../controllers/screen') + +const router = express.Router() + +router.get('/', getAllScreens) + +router.post('/', createScreen) + +module.exports = router \ No newline at end of file diff --git a/src/routers/screening.js b/src/routers/screening.js new file mode 100644 index 00000000..041f142f --- /dev/null +++ b/src/routers/screening.js @@ -0,0 +1,8 @@ +const express = require("express") +const { getAllScreenings } = require('../controllers/screening') + +const router = express.Router() + +router.get('/', getAllScreenings) + +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..92b8143e --- /dev/null +++ b/src/routers/ticket.js @@ -0,0 +1,11 @@ +const express = require("express") + +const { createTicket, getAllTickets } = require('../controllers/ticket') + +const router = express.Router() + +router.get('/', getAllTickets) + +router.post('/', createTicket) + +module.exports = router \ No newline at end of file diff --git a/src/server.js b/src/server.js index 93d47a16..b5b17ab6 100644 --- a/src/server.js +++ b/src/server.js @@ -1,21 +1,54 @@ -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 })) // Tell express to use your routers here -const customerRouter = require('./routers/customer'); -app.use('/customers', customerRouter); +const customerRouter = require("./routers/customer") +const moviesRouter = require("./routers/movie") +const screensRouter = require("./routers/screen") +const ticketsRouter = require('./routers/ticket') +const screeningsRouter = require('./routers/screening') +const reviewsRouter = require('./routers/review.js') + +const { + MissingFieldsError, + ExistingDataError, + DataNotFoundError, +} = require("./errors/errors.js") + +app.use("/customers", customerRouter) +app.use("/movies", moviesRouter) +app.use("/screens", screensRouter) +app.use("/tickets", ticketsRouter) +app.use('/screenings', screeningsRouter) +app.use('/reviews', reviewsRouter) + +app.use((error, req, res, next) => { + if (error instanceof MissingFieldsError) { + return res.status(400).json({ error: error.message }) + } + if (error instanceof DataNotFoundError) { + return res.status(404).json({ error: error.message }) + } + if (error instanceof ExistingDataError) { + return res.status(409).json({ error: error.message }) + } + console.error(error) + res.status(500).json({ + message: "Something went wrong", + }) +}) module.exports = app diff --git a/test/api/extensions/movie-ext.spec.js b/test/api/extensions/movie-ext.spec.js new file mode 100644 index 00000000..9e772c34 --- /dev/null +++ b/test/api/extensions/movie-ext.spec.js @@ -0,0 +1,292 @@ +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?runtimeLt", () => { + it("will get a list of movies filterd by provided_runtimeMins < runtimeLt", async () => { + const scr1 = await createScreen(1) + const scr2 = await createScreen(2) + const scr3 = await createScreen(3) + await createMovie("Movie 1", 111, scr1) + await createMovie("Movie 2", 222, scr2) + await createMovie("Movie 3", 333, scr3) + + const response = await supertest(app).get( + "/movies?runtimeLt=300" + ) + + console.log(response.body) + 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("Movie 1") + expect(movie1.runtimeMins).toEqual(111) + expect(movie2.title).toEqual("Movie 2") + expect(movie2.runtimeMins).toEqual(222) + }) + }) + + describe("GET /movies?runtimeGt", () => { + it("will get a list of movies filterd by provided_runtimeMins > runtimeGt", async () => { + const scr1 = await createScreen(1) + const scr2 = await createScreen(2) + const scr3 = await createScreen(3) + await createMovie("Movie 1", 111, scr1) + await createMovie("Movie 2", 222, scr2) + await createMovie("Movie 3", 333, scr3) + + const response = await supertest(app).get( + "/movies?runtimeGt=200" + ) + + 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("Movie 2") + expect(movie1.runtimeMins).toEqual(222) + expect(movie2.title).toEqual("Movie 3") + expect(movie2.runtimeMins).toEqual(333) + }) + }) + + describe("GET /movies?runtimeLt&runtimeGt", () => { + it("will get a list of movies filterd by runtimeLt > provided_runtimeMins > runtimeGt", async () => { + const scr1 = await createScreen(1) + const scr2 = await createScreen(2) + const scr3 = await createScreen(3) + await createMovie("Movie 1", 111, scr1) + await createMovie("Movie 2", 222, scr2) + await createMovie("Movie 3", 333, scr3) + const response = await supertest(app).get( + "/movies?runtimeLt=300&runtimeGt=200" + ) + + expect(response.status).toEqual(200) + expect(response.body.movies).not.toEqual(undefined) + expect(response.body.movies.length).toEqual(1) + + const [movie1] = response.body.movies + expect(movie1.title).toEqual("Movie 2") + expect(movie1.runtimeMins).toEqual(222) + }) + }) + + describe("POST /movies", () => { + it("will add a movie with screenings if provided", async () => { + const scr1 = await createScreen(1) + const scr2 = await createScreen(2) + + const request = { + title: "Movie 1", + runtimeMins: 111, + screenings: [ + { + movieId: 1, + screenId: scr1.id, + startsAt: Date.now(), + }, + { + movieId: 1, + screenId: scr2.id, + startsAt: Date.now(), //"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("Movie 1") + expect(response.body.movie.runtimeMins).toEqual(111) + expect(response.body.movie.screenings).not.toEqual(undefined) + expect(response.body.movie.screenings.length).toEqual(2) + }) + + it("will be still able to add a movie if screenings are not provided", async () => { + const request = { + title: "Movie 1", + runtimeMins: 111, + } + + 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("Movie 1") + expect(response.body.movie.runtimeMins).toEqual(111) + expect(response.body.movie.screenings).not.toEqual(undefined) + expect(response.body.movie.screenings.length).toEqual(0) + }) + + it("will have status 400 and return an error if the request is missing fields", async () => { + const request = { + title: "Movie 1", + } + + const response = await supertest(app) + .post("/movies") + .send(request) + + expect(response.status).toEqual(400) + expect(response.body).toHaveProperty("error") + expect(response.body.error).toEqual( + "Title and duration in minutes must be provided in order to add a movie" + ) + }) + + it("will have status 409 and return an error if adding existing movie", async () => { + const movie = await createMovie("Movie 1", 120) + + const request = { + title: "Movie 1", + runtimeMins: 111, + } + + const response = await supertest(app) + .post("/movies") + .send(request) + + expect(response.status).toEqual(409) + expect(response.body).toHaveProperty("error") + expect(response.body.error).toEqual( + "A movie with the provided title already exists" + ) + }) + }) + + describe("GET /movies/:idOrTitle", () => { + it("will get a movie by title if it exists", async () => { + const screen = await createScreen(1) + const movie = await createMovie("Movie 1", 111, 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("Movie 1") + expect(response.body.movie.runtimeMins).toEqual(111) + expect(response.body.movie.screenings).not.toEqual(undefined) + expect(response.body.movie.screenings.length).toEqual(1) + }) + + it("will have status 404 if a movie with the provided title does not exist", async () => { + const movie = await createMovie("Movie 1", 111) + + const response = await supertest(app).get("/movies/WrongTitle") + + expect(response.status).toEqual(404) + expect(response.body).toHaveProperty("error") + expect(response.body.error).toEqual( + "No movie with the provided id or title exists" + ) + }) + + it("will have status 404 if a movie with the provided id does not exist", async () => { + const movie = await createMovie("Movie 1", 111) + + const response = await supertest(app).get("/movies/11111") + + expect(response.status).toEqual(404) + expect(response.body).toHaveProperty("error") + expect(response.body.error).toEqual( + "No movie with the provided id or title exists" + ) + }) + }) + + describe("PUT /movies/:id", () => { + it("will update a movie by id and add screenings if provided", async () => { + const scr1 = await createScreen(1) + const scr2 = await createScreen(2) + const movie = await createMovie("Movie 1", 123, scr1) + + const request = { + title: "Movie 2", + runtimeMins: 111, + screenings: [ + { + screenId: scr2.id, + startsAt: Date.now(), + }, + ], + } + + const response = await supertest(app) + .put(`/movies/${movie.id}`) + .send(request) + + expect(response.status).toEqual(201) + expect(response.body.movie).not.toEqual(undefined) + expect(response.body.movie.title).toEqual("Movie 2") + expect(response.body.movie.runtimeMins).toEqual(111) + expect(response.body.movie.screenings).not.toEqual(undefined) + expect(response.body.movie.screenings.length).toEqual(1) + }) + + it("will have status 404 if trying to update a non existing movie", async () => { + const scr1 = await createScreen(1) + const scr2 = await createScreen(2) + const movie = await createMovie("Movie 1", 123, scr1) + + const request = { + title: "Movie 2", + runtimeMins: 111, + screenings: [ + { + screenId: scr2.id, + startsAt: Date.now(), + }, + ], + } + + const response = await supertest(app) + .put(`/movies/3`) + .send(request) + + expect(response.status).toEqual(404) + expect(response.body).toHaveProperty("error") + expect(response.body.error).toEqual( + "No movie with the provided ID exists" + ) + }) + + it("will return 400 and return an error if the request is missing fields", async () => { + const scr1 = await createScreen(1) + const scr2 = await createScreen(2) + const movie = await createMovie("Movie 1", 123, scr1) + + const request = { + title: "Movie 2", + screenings: [ + { + screenId: scr2.id, + startsAt: Date.now(), + }, + ], + } + + const response = await supertest(app) + .put(`/movies/${movie.id}`) + .send(request) + + expect(response.status).toEqual(400) + expect(response.body).toHaveProperty("error") + expect(response.body.error).toEqual( + "Title and duration in minutes must be provided in order to update a movie" + ) + }) + }) +}) diff --git a/test/api/extensions/screen-ext.spec.js b/test/api/extensions/screen-ext.spec.js new file mode 100644 index 00000000..ffc11962 --- /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 provided in the request", async () => { + const movie1 = await createMovie("Movie 1", 111) + const movie2 = await createMovie("Movie 2", 222) + + const request = { + number: 10, + screenings: [ + { + movieId: movie1.id, + startsAt: Date.now(), + }, + { + movieId: movie2.id, + startsAt: Date.now(), + }, + ], + } + + 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 have status 400 and return an error if the request is missing fields", async () => { + const request = {} + + const response = await supertest(app) + .post("/movies") + .send(request) + + expect(response.status).toEqual(400) + expect(response.body).toHaveProperty("error") + }) + + it("will have status 409 and return an error if adding existing movie", 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") + }) + }) +}) diff --git a/test/helpers/createMovie.js b/test/helpers/createMovie.js index 06903fea..2e97e65b 100644 --- a/test/helpers/createMovie.js +++ b/test/helpers/createMovie.js @@ -15,7 +15,7 @@ const createMovie = async (title, runtimeMins, screen = null) => { movieData.data.screenings = { create: [ { - startsAt: "2022-06-11T18:30:00.000Z", + startsAt: "2025-06-11T18:30:00.000Z", screenId: screen.id } ]