From 5fa8cb6e553074d23d3061f0edde577566a117eb Mon Sep 17 00:00:00 2001 From: Hamada Abdelaal Date: Thu, 4 Jul 2024 12:41:14 +0200 Subject: [PATCH] standard --- .env.example | 6 - .../migration.sql | 8 ++ .../migration.sql | 8 ++ prisma/schema.prisma | 68 +++++----- prisma/seed.js | 48 ++++---- .../{customer.js => customerCon.js} | 61 ++++++--- src/controllers/movieCon.js | 116 ++++++++++++++++++ src/controllers/screenCon.js | 26 ++++ src/domains/customer.js | 26 ---- src/domains/customerDom.js | 48 ++++++++ src/domains/movieDom.js | 71 +++++++++++ src/domains/screenDom.js | 15 +++ src/routers/customer.js | 13 -- src/routers/customerRouter.js | 15 +++ src/routers/movieRouter.js | 15 +++ src/routers/screenRouter.js | 8 ++ src/server.js | 21 ++-- 17 files changed, 444 insertions(+), 129 deletions(-) delete mode 100644 .env.example create mode 100644 prisma/migrations/20240703172621_optional_screenings/migration.sql create mode 100644 prisma/migrations/20240704103311_update_screenings/migration.sql rename src/controllers/{customer.js => customerCon.js} (57%) create mode 100644 src/controllers/movieCon.js create mode 100644 src/controllers/screenCon.js delete mode 100644 src/domains/customer.js create mode 100644 src/domains/customerDom.js create mode 100644 src/domains/movieDom.js create mode 100644 src/domains/screenDom.js delete mode 100644 src/routers/customer.js create mode 100644 src/routers/customerRouter.js create mode 100644 src/routers/movieRouter.js create mode 100644 src/routers/screenRouter.js 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/migrations/20240703172621_optional_screenings/migration.sql b/prisma/migrations/20240703172621_optional_screenings/migration.sql new file mode 100644 index 00000000..9bcbef0e --- /dev/null +++ b/prisma/migrations/20240703172621_optional_screenings/migration.sql @@ -0,0 +1,8 @@ +-- DropForeignKey +ALTER TABLE "Screening" DROP CONSTRAINT "Screening_movieId_fkey"; + +-- AlterTable +ALTER TABLE "Screening" ALTER COLUMN "movieId" DROP NOT NULL; + +-- AddForeignKey +ALTER TABLE "Screening" ADD CONSTRAINT "Screening_movieId_fkey" FOREIGN KEY ("movieId") REFERENCES "Movie"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20240704103311_update_screenings/migration.sql b/prisma/migrations/20240704103311_update_screenings/migration.sql new file mode 100644 index 00000000..4b41c10d --- /dev/null +++ b/prisma/migrations/20240704103311_update_screenings/migration.sql @@ -0,0 +1,8 @@ +-- DropForeignKey +ALTER TABLE "Screening" DROP CONSTRAINT "Screening_screenId_fkey"; + +-- AlterTable +ALTER TABLE "Screening" ALTER COLUMN "screenId" DROP NOT NULL; + +-- AddForeignKey +ALTER TABLE "Screening" ADD CONSTRAINT "Screening_screenId_fkey" FOREIGN KEY ("screenId") REFERENCES "Screen"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dd9b27f1..3ab5a2aa 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/prisma/seed.js b/prisma/seed.js index 31e28bfa..7ccedc58 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -1,4 +1,4 @@ -const { PrismaClient } = require('@prisma/client'); +const { PrismaClient } = require("@prisma/client"); const prisma = new PrismaClient(); async function seed() { @@ -13,28 +13,28 @@ async function seed() { async function createCustomer() { const customer = await prisma.customer.create({ data: { - name: 'Alice', + name: "Alice", contact: { create: { - email: 'alice@boolean.co.uk', - phone: '1234567890' - } - } + email: "alice@boolean.co.uk", + phone: "1234567890", + }, + }, }, include: { - contact: true - } + contact: true, + }, }); - console.log('Customer created', customer); + console.log("Customer created", customer); return customer; } async function createMovies() { const rawMovies = [ - { title: 'The Matrix', runtimeMins: 120 }, - { title: 'Dodgeball', runtimeMins: 154 }, + { title: "The Matrix", runtimeMins: 120 }, + { title: "Dodgeball", runtimeMins: 154 }, ]; const movies = []; @@ -44,24 +44,22 @@ async function createMovies() { movies.push(movie); } - console.log('Movies created', movies); + console.log("Movies created", movies); return movies; } async function createScreens() { - const rawScreens = [ - { number: 1 }, { number: 2 } - ]; + const rawScreens = [{ number: 1 }, { number: 2 }]; const screens = []; for (const rawScreen of rawScreens) { const screen = await prisma.screen.create({ - data: rawScreen + data: rawScreen, }); - console.log('Screen created', screen); + console.log("Screen created", screen); screens.push(screen); } @@ -81,24 +79,24 @@ async function createScreenings(screens, movies) { startsAt: screeningDate, movie: { connect: { - id: movies[i].id - } + id: movies[i].id, + }, }, screen: { connect: { - id: screen.id - } - } - } + id: screen.id, + }, + }, + }, }); - console.log('Screening created', screening); + console.log("Screening created", screening); } } } seed() - .catch(async e => { + .catch(async (e) => { console.error(e); await prisma.$disconnect(); }) diff --git a/src/controllers/customer.js b/src/controllers/customerCon.js similarity index 57% rename from src/controllers/customer.js rename to src/controllers/customerCon.js index 775cfb42..f98fc577 100644 --- a/src/controllers/customer.js +++ b/src/controllers/customerCon.js @@ -1,17 +1,16 @@ -const { PrismaClientKnownRequestError } = require("@prisma/client") -const { createCustomerDb } = require('../domains/customer.js') +const { PrismaClientKnownRequestError } = require("@prisma/client"); +const { + createCustomerDb, + updateCustomerDb, +} = require("../domains/customerDom.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. @@ -22,9 +21,9 @@ const createCustomer = async (req, res) => { // 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 @@ -35,14 +34,44 @@ const createCustomer = async (req, res) => { // 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 id = Number.parseInt(req.params.id, 10); + const { name, phone, email } = req.body; + + if (!name) { + return res.status(400).json({ + error: "Missing fields in request body", + }); + } + + try { + const updateCustomer = await updateCustomerDb(name, phone, email, id); + + res.status(201).json({ customer: updateCustomer }); + } 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 }); + } +}; module.exports = { - createCustomer -} + createCustomer, + updateCustomer, +}; diff --git a/src/controllers/movieCon.js b/src/controllers/movieCon.js new file mode 100644 index 00000000..a7a99f6e --- /dev/null +++ b/src/controllers/movieCon.js @@ -0,0 +1,116 @@ +const { PrismaClientKnownRequestError } = require("@prisma/client"); +const { + getMoviesDb, + getMovieDb, + createMovieDb, + updateMovieDb, + deleteMovieDb, +} = require("../domains/movieDom.js"); + +const getMovies = async (req, res) => { + try { + const movies = await getMoviesDb(); + + res.status(200).json({ movies }); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === "P2021") { + return res + .status(404) + .json({ error: "The table does not exist in the current database" }); + } + } + + res.status(500).json({ error: e.message }); + } +}; + +const createMovie = async (req, res) => { + const { body } = req; + + if (!body.title || !body.runtimeMins) { + return res.status(400).json({ + error: "Missing fields in request body", + }); + } + + try { + const newMovie = await createMovieDb(body); + + res.status(201).json({ movie: newMovie }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}; + +const getMovie = async (req, res) => { + const id = Number.parseInt(req.params.id, 10); + + try { + const movie = await getMovieDb(id); + + res.status(200).json({ movie }); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.name === "NotFoundError") { + return res + .status(404) + .json({ error: `The movie with ID ${id} does not exist` }); + } + } + res.status(500).json({ error: e.message }); + } +}; + +const updateMovie = async (req, res) => { + const id = Number.parseInt(req.params.id, 10); + const { body } = req; + + if (!body.title || !body.runtimeMins) { + return res.status(400).json({ + error: "Missing fields in request body", + }); + } + + try { + const updatedMovie = await updateMovieDb(id, body); + + res.status(201).json({ movie: updatedMovie }); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === "P2025") { + return res + .status(404) + .json({ error: `The movie with ID ${id} does not exist` }); + } + } + res.status(500).json({ error: e.message }); + } +}; + +const deleteMovie = async (req, res) => { + const id = Number.parseInt(req.params.id, 10); + + try { + const deletedMovie = await deleteMovieDb(id); + + res.status(200).json({ movie: deletedMovie }); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === "P2025") { + return res + .status(404) + .json({ error: `The movie with ID ${id} does not exist` }); + } + } + res.status(500).json({ error: e.message }); + } +}; + +module.exports = { + getMovies, + getMovie, + createMovie, + updateMovie, + deleteMovie, +}; diff --git a/src/controllers/screenCon.js b/src/controllers/screenCon.js new file mode 100644 index 00000000..8bd95a84 --- /dev/null +++ b/src/controllers/screenCon.js @@ -0,0 +1,26 @@ +const { PrismaClientKnownRequestError } = require("@prisma/client"); +const { createScreenDb } = require("../domains/screenDom.js"); + +const createScreen = async (req, res) => { + const { body } = req; + + try { + const newScreen = await createScreenDb(body); + + res.status(201).json({ screen: newScreen }); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + if (e.code === "P2021") { + return res + .status(404) + .json({ error: "The table does not exist in the current database" }); + } + } + + res.status(500).json({ error: e.message }); + } +}; + +module.exports = { + createScreen, +}; diff --git a/src/domains/customer.js b/src/domains/customer.js deleted file mode 100644 index c7f315fd..00000000 --- a/src/domains/customer.js +++ /dev/null @@ -1,26 +0,0 @@ -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 - } -}) - -module.exports = { - createCustomerDb -} diff --git a/src/domains/customerDom.js b/src/domains/customerDom.js new file mode 100644 index 00000000..3cf4fabc --- /dev/null +++ b/src/domains/customerDom.js @@ -0,0 +1,48 @@ +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 updateCustomerDb = async (name, phone, email, id) => { + return await prisma.customer.update({ + where: { + id, + }, + data: { + name, + contact: { + update: { + phone, + email, + }, + }, + }, + include: { + contact: true, + }, + }); +}; + +module.exports = { + createCustomerDb, + updateCustomerDb, +}; diff --git a/src/domains/movieDom.js b/src/domains/movieDom.js new file mode 100644 index 00000000..c4f4596f --- /dev/null +++ b/src/domains/movieDom.js @@ -0,0 +1,71 @@ +const prisma = require("../utils/prisma"); + +const getMoviesDb = async () => { + return await prisma.movie.findMany({ + include: { + screenings: true, + }, + }); +}; + +const createMovieDb = async (data) => { + const result = await prisma.movie.create({ + data, + include: { + screenings: true, + }, + }); + return result; +}; + +const getMovieDb = async (id) => { + const result = await prisma.movie.findUniqueOrThrow({ + where: { + id, + }, + include: { + screenings: true, + }, + }); + return result; +}; + +const updateMovieDb = async (id, data) => { + const result = await prisma.movie.update({ + where: { + id, + }, + data, + include: { + screenings: true, + }, + }); + return result; +}; + +const deleteMovieDb = async (id) => { + const deleteScreenings = prisma.screening.deleteMany({ + where: { + movieId: id, + }, + }); + + const deleteMovie = prisma.movie.delete({ + where: { + id, + }, + }); + + const transaction = await prisma.$transaction([ + deleteScreenings, + deleteMovie, + ]); +}; + +module.exports = { + getMoviesDb, + getMovieDb, + createMovieDb, + updateMovieDb, + deleteMovieDb, +}; diff --git a/src/domains/screenDom.js b/src/domains/screenDom.js new file mode 100644 index 00000000..f8524dc1 --- /dev/null +++ b/src/domains/screenDom.js @@ -0,0 +1,15 @@ +const prisma = require("../utils/prisma"); + +const createScreenDb = async (data) => { + const result = await prisma.screen.create({ + data, + include: { + screenings: true, + }, + }); + return result; +}; + +module.exports = { + createScreenDb, +}; diff --git a/src/routers/customer.js b/src/routers/customer.js deleted file mode 100644 index f14a87fc..00000000 --- a/src/routers/customer.js +++ /dev/null @@ -1,13 +0,0 @@ -const express = require("express"); -const { - createCustomer -} = 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); - -module.exports = router; diff --git a/src/routers/customerRouter.js b/src/routers/customerRouter.js new file mode 100644 index 00000000..ac7f14ac --- /dev/null +++ b/src/routers/customerRouter.js @@ -0,0 +1,15 @@ +const express = require("express"); +const { + createCustomer, + updateCustomer, +} = require("../controllers/customerCon"); + +const router = express.Router(); + +// In server.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/customers/register +router.post("/register", createCustomer); +router.put("/:id", updateCustomer); + +module.exports = router; diff --git a/src/routers/movieRouter.js b/src/routers/movieRouter.js new file mode 100644 index 00000000..13d74639 --- /dev/null +++ b/src/routers/movieRouter.js @@ -0,0 +1,15 @@ +const express = require("express"); +const { + getMovies, + createMovie, + getMovie, + updateMovie, + deleteMovie, +} = require("../controllers/movieCon"); + +const router = express.Router(); + +router.route("/").get(getMovies).post(createMovie); +router.route("/:id").get(getMovie).put(updateMovie).delete(deleteMovie); + +module.exports = router; diff --git a/src/routers/screenRouter.js b/src/routers/screenRouter.js new file mode 100644 index 00000000..b5c40c7c --- /dev/null +++ b/src/routers/screenRouter.js @@ -0,0 +1,8 @@ +const express = require("express"); +const { createScreen } = require("../controllers/screenCon"); + +const router = express.Router(); + +router.route("/").post(createScreen); + +module.exports = router; diff --git a/src/server.js b/src/server.js index 93d47a16..4fa94bd1 100644 --- a/src/server.js +++ b/src/server.js @@ -1,21 +1,24 @@ -const express = require('express'); +const express = require("express"); 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(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/customerRouter"); +const movieRouter = require("./routers/movieRouter"); +const screenRouter = require("./routers/screenRouter"); +app.use("/customers", customerRouter); +app.use("/movies", movieRouter); +app.use("/screens", screenRouter); -module.exports = app +module.exports = app;