diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1d07b9e..50bae8e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -34,11 +34,13 @@ model Admin { } model Discipline { - id Int @id @default(autoincrement()) - pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid - name String - minTeamSize Int - maxTeamSize Int + id Int @id @default(autoincrement()) + pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid + name String + briefDescription String + fullDescription String? + minTeamSize Int + maxTeamSize Int roles RoleSchema[] teams Team[] @@ -60,15 +62,15 @@ model RoleSchema { } model Team { - id Int @id @default(autoincrement()) - pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid + id Int @id @default(autoincrement()) + pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid name String leaderEmail String verified Boolean @default(false) - roles Role[] @relation(name: "participants") + roles Role[] @relation(name: "participants") participants Participant[] - discipline Discipline @relation(fields: [disciplineId], references: [id], onDelete: Cascade) + discipline Discipline @relation(fields: [disciplineId], references: [id], onDelete: Cascade) disciplineId Int } @@ -81,7 +83,7 @@ model Participant { group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) groupId Int - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) teamId Int roles Role[] } @@ -125,7 +127,7 @@ model Group { model Media { id Int @id @default(autoincrement()) pid String @unique - description String + description String @default("visual") events Event[] disciplines Discipline[] diff --git a/src/Controllers/discipline.controller.ts b/src/Controllers/discipline.controller.ts index 40d5494..6251a22 100644 --- a/src/Controllers/discipline.controller.ts +++ b/src/Controllers/discipline.controller.ts @@ -1,10 +1,10 @@ import { Prisma, Organisation, Admin, AdminLevel, Team } from "@prisma/client"; import { PrismaClientKnownRequestError, PrismaClientUnknownRequestError } from "@prisma/client/runtime"; import { Request, Response } from "express"; -import { z } from "zod"; import prisma from "../lib/prisma"; import ForwardableError from "../Middleware/error/ForwardableError"; import NotFoundError from "../Middleware/error/NotFoundError"; +import { number, z } from "zod"; import { createInsufficientPermissionsError, DataType, @@ -36,6 +36,8 @@ const basicDiscipline = { visual: { select: { pid: true } }, maxTeamSize: true, minTeamSize: true, + briefDescription: true, + fullDescription: true, event: { select: { pid: true, name: true } }, roles: { select: { pid: true, name: true } }, } as const; @@ -128,10 +130,90 @@ export const getDiscipline = async (req: Request, res: }); }; +export const updateDiscipline = async (req: Request<{ pid: string }>, res: Response) => { + if (req.auth?.permission_level !== "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const { pid } = req.params; + + const result = UpdateDisciplineBody.safeParse(req.body); + + if (result.success === false) { + return res.status(400).json( + generateInvalidBodyError({ + name: DataType.STRING, + minTeamSize: DataType.NUMBER, + maxTeamSize: DataType.NUMBER, + briefDescription: DataType.STRING, + ["fullDescription?"]: DataType.STRING, + }) + ); + } + + const body = result.data; + + try { + const discipline = await prisma.discipline.update({ + where: { pid }, + data: { + name: body.name, + minTeamSize: body.minTeamSize, + maxTeamSize: body.maxTeamSize, + briefDescription: body.briefDescription, + fullDescription: body.fullDescription, + }, + select: { + pid: true, + name: true, + minTeamSize: true, + maxTeamSize: true, + briefDescription: true, + fullDescription: true, + }, + }); + + if (!discipline) { + throw new NotFoundError("discipline", pid); + } + + res.status(200).json({ + type: "success", + payload: { + discipline, + }, + }); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + return res.status(500).json({ + type: "error", + payload: { + message: `Internal Server error occured. Try again later`, + }, + }); + } + if (e instanceof Prisma.PrismaClientUnknownRequestError) { + return res.status(500).json({ + type: "error", + payload: { + message: "Unknown error occurred with your request. Check if your parameters are correct", + schema: { + eventId: DataType.UUID, + }, + }, + }); + } + + throw e; + } +}; + interface CreateDisciplineBody { name?: string; minTeamSize?: number; maxTeamSize?: number; + briefDescription?: string; + fullDescription?: string; } // require: auth(ELEVATED) @@ -241,63 +323,3 @@ export const deleteDiscipline = async (req: Request<{ pid: string }>, res: Respo throw e; } }; - -interface visualParams { - disciplinePid: string; -} - -interface visualBody { - mediaPid: string; -} - -export const addVisual = async (req: Request, res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { disciplinePid } = req.params; - - const discipline = await prisma.discipline.update({ - where: { pid: disciplinePid }, - data: { - visual: { connect: { pid: req.body.mediaPid } }, - }, - }); - - // TODO: This does not work and should be updated in all addVisual-type code segments - // Reason: update throw a PrismaClientKnownRequestError with code P2025 if the record to update could not be found - if (!discipline) { - throw new NotFoundError("discipline", disciplinePid); - } - - return res.status(200).json({ - type: "success", - payload: {}, - }); -}; - -export const deleteVisual = async (req: Request, res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { disciplinePid, pid } = req.params; - - try { - await prisma.discipline.update({ - where: { - pid: disciplinePid, - }, - data: { - visual: { disconnect: { pid } }, - }, - }); - return res.status(204).end(); - } catch (e) { - if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("discipline", disciplinePid); // Refer: Last todo; This is a correct example - } - - throw e; - } -}; diff --git a/src/Controllers/event.controller.ts b/src/Controllers/event.controller.ts index 03d0426..1e558c7 100644 --- a/src/Controllers/event.controller.ts +++ b/src/Controllers/event.controller.ts @@ -1,6 +1,6 @@ import { Prisma } from "@prisma/client"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; -import { Request, Response } from "express"; +import e, { Request, Response } from "express"; import { z } from "zod"; import prisma from "../lib/prisma"; import NotFoundError from "../Middleware/error/NotFoundError"; @@ -8,16 +8,39 @@ import { createInsufficientPermissionsError, DataType, generateError, generateIn require("express-async-errors"); +const EventBody = z.object({ + name: z.string(), + date: z.string(), + briefDescription: z.string(), + fullDescription: z.string(), +}); + +const UpdateBody = EventBody.partial(); + +const CreateEventBody = EventBody.partial({ + fullDescription: true, +}); + export const getAllEvents = async (req: Request, res: Response) => { const events = await prisma.event.findMany({ select: { + pid: true, name: true, + date: true, briefDescription: true, fullDescription: true, visual: { select: { pid: true, description: true } }, - date: true, - pid: true, - id: false, + disciplines: { + select: { + pid: true, + name: true, + }}, + organisations: { + select: { + pid: true, + name: true, + } + } }, }); @@ -46,13 +69,25 @@ export const getEvent = async (req: Request, res: Response) => { pid: eventId, }, select: { + pid: true, name: true, + date: true, briefDescription: true, fullDescription: true, - date: true, - pid: true, - id: false, visual: { select: { pid: true, description: true } }, + disciplines: { + select: { + pid: true, + name: true, + briefDescription: true, + fullDescription: true, + }}, + organisations: { + select: { + pid: true, + name: true, + } + } }, }); @@ -96,12 +131,10 @@ export const addEvent = async (req: Request, res: Response) => { if (req.auth?.permission_level !== "ELEVATED") { res.status(403).json(createInsufficientPermissionsError()); } - if ( - typeof req.body.name !== "string" || - typeof req.body.date !== "string" || - typeof req.body.briefDescription !== "string" || - (req.body.fullDescription && typeof req.body.fullDescription !== "string") - ) { + + const result = CreateEventBody.safeParse(req.body); + + if(result.success === false){ return res.status(400).json( generateInvalidBodyError({ name: DataType.STRING, @@ -122,10 +155,9 @@ export const addEvent = async (req: Request, res: Response) => { fullDescription: req.body.fullDescription, }, select: { + pid: true, name: true, date: true, - pid: true, - id: false, briefDescription: true, fullDescription: true, }, @@ -139,15 +171,6 @@ export const addEvent = async (req: Request, res: Response) => { }); }; -const EventBody = z.object({ - name: z.string(), - date: z.string(), - briefDescription: z.string(), - fullDescription: z.string(), -}); - -const UpdateBody = EventBody.partial(); - export const updateEvent = async (req: Request<{ pid: string }>, res: Response) => { if (req.auth?.permission_level !== "ELEVATED") { res.status(403).json(createInsufficientPermissionsError()); @@ -250,67 +273,3 @@ export const deleteEvent = async (req: Request, res: Res throw e; } }; - -// REVIEW: This code **will** need to be de-duplicated - -interface visualParams { - eventPid: string; -} - -interface visualBody { - mediaPid: string; -} - -export const addVisual = async (req: Request, res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { eventPid } = req.params; - - if (typeof req.body.mediaPid !== "string") { - res.status(400).json(generateInvalidBodyError({ mediaPid: DataType.STRING })); - } - - const event = await prisma.event.update({ - where: { pid: eventPid }, - data: { - visual: { connect: { pid: req.body.mediaPid } }, - }, - }); - - if (!event) { - throw new NotFoundError("event", eventPid); - } - - return res.status(200).json({ - type: "success", - payload: {}, - }); -}; - -export const deleteVisual = async (req: Request, res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { eventPid, pid } = req.params; - - try { - await prisma.event.update({ - where: { - pid: eventPid, - }, - data: { - visual: { disconnect: { pid } }, - }, - }); - return res.status(204).end(); - } catch (e) { - if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("discipline", eventPid); - } - - throw e; - } -}; diff --git a/src/Controllers/media.controller.ts b/src/Controllers/media.controller.ts index eb78bc3..22802a7 100644 --- a/src/Controllers/media.controller.ts +++ b/src/Controllers/media.controller.ts @@ -1,4 +1,4 @@ -import { Request, response, Response } from "express"; +import { NextFunction, Request, response, Response } from "express"; import fs from "fs"; import isSvg from "is-svg"; import { fromBuffer as fileTypeFromBuffer } from "file-type"; @@ -12,6 +12,8 @@ import { type } from "os"; import { unlink } from "fs/promises"; import ForwardableError from "../Middleware/error/ForwardableError"; import SchemaError from "../Middleware/error/SchemaError"; +import { z } from "zod"; +import { updateEvent } from "./event.controller"; require("express-async-errors"); @@ -189,3 +191,87 @@ export const deleteMedia = async (req: Request, res: Response) => { throw e; } }; + +//TODO: maybe create a function that adds the tableToUpdate based on path +// and call it before calling (un)linkMedia + +export const linkMedia = async ( + req: Request<{ pid: string }, {}, { mediaPid: string }>, + res: Response) => { + if (req.auth?.permission_level != "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const { pid } = req.params; + const { mediaPid } = req.body; + const tableToUpdate = req.originalUrl.split("/"); + + if(typeof mediaPid !== "string") { + return res.status(400).json( + generateInvalidBodyError({ + mediaPid: DataType.UUID, + }) + ); + } + + const updatedRec = await getPrismaUpdateFKT(tableToUpdate[2])({ + where: { pid }, + data: { + visual: { connect: { pid: mediaPid } }, + }, + }); + + if (!updatedRec) { + throw new NotFoundError(tableToUpdate[2], pid); + } + + return res.status(200).json({ + type: "success", + payload: {}, + }); +}; + +export const unlinkMedia = async ( + req: Request<{ pid: string, mediaPid: string }>, + res: Response) => { + if (req.auth?.permission_level != "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const { pid, mediaPid } = req.params; + const tableToUpdate = req.originalUrl.split("/"); + + try { + await getPrismaUpdateFKT(tableToUpdate[2])({ + where: { pid }, + data: { visual: { disconnect: { pid: mediaPid } } }, + }); + return res.status(204).end(); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + console.log("not found error"); + throw new NotFoundError(tableToUpdate[2], pid); + } + if (e instanceof Prisma.PrismaClientUnknownRequestError) { + return res.status(500).json({ + type: "error", + payload: { + message: "Unknown error occurred with your request. Check if your parameters are correct", + schema: { + eventId: DataType.UUID, + }, + }, + }); + } + + throw e; + } +}; + +function getPrismaUpdateFKT( tableToUpdate: string ): Function { + switch(tableToUpdate) { + case "events": return prisma.event.update; + case "disciplines": return prisma.discipline.update; + default: return prisma.roleSchema.update; + } +} diff --git a/src/Controllers/role_schema.controller.ts b/src/Controllers/role_schema.controller.ts index f47e154..8b05caf 100644 --- a/src/Controllers/role_schema.controller.ts +++ b/src/Controllers/role_schema.controller.ts @@ -133,129 +133,3 @@ export const createRoleSchema = async ( throw e; } }; - -export const updateRoleSchema = async (req: Request<{ pid: string }>, res: Response) => { - if (req.auth?.permission_level !== "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { pid } = req.params; - - const result = UpdateRoleSchema.safeParse(req.body); - - if (result.success === false) { - return res.status(400).json( - generateInvalidBodyError( - { - name: DataType.STRING, - schema: DataType.RESULT_SCHEMA, - }, - result.error - ) - ); - } - - const { name, schema } = result.data; - - const validatedSchema = parseSchema(schema); - - try { - const schema = await prisma.roleSchema.update({ - where: { pid }, - data: { - name: name, - schema: validatedSchema, - }, - select: roleSchema, - }); - - res.status(200).json({ - type: "success", - payload: schema, - }); - } catch (e) { - if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("roleSchema", pid); - } - - throw e; - } -}; - -export const deleteRoleSchema = async (req: Request<{ pid: string }>, res: Response) => { - if (req.auth?.permission_level !== "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { pid } = req.params; - - try { - await prisma.roleSchema.delete({ where: { pid } }); - - return res.status(204).end(); - } catch (e) { - if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - return res.status(404).json(generateError(`The RoleSchema with the ID ${pid} could not be found`)); - } - - throw e; - } -}; - -interface visualParams { - schemaPid: string; -} - -interface visualBody { - mediaPid: string; -} - -export const addVisual = async (req: Request, res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { schemaPid } = req.params; - - const schema = await prisma.roleSchema.update({ - where: { pid: schemaPid }, - data: { - visual: { connect: { pid: req.body.mediaPid } }, - }, - }); - - if (!schema) { - throw new NotFoundError("role_schema", schemaPid); - } - - return res.status(200).json({ - type: "success", - payload: {}, - }); -}; - -export const deleteVisual = async (req: Request, res: Response) => { - if (req.auth?.permission_level != "ELEVATED") { - res.status(403).json(createInsufficientPermissionsError()); - } - - const { schemaPid, pid } = req.params; - - try { - await prisma.roleSchema.update({ - where: { - pid: schemaPid, - }, - data: { - visual: { disconnect: { pid } }, - }, - }); - return res.status(204).end(); - } catch (e) { - if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { - throw new NotFoundError("role_schema", schemaPid); - } - - throw e; - } -}; diff --git a/src/Routes/discipline.routes.ts b/src/Routes/discipline.routes.ts index 50a2de9..6111c64 100644 --- a/src/Routes/discipline.routes.ts +++ b/src/Routes/discipline.routes.ts @@ -1,10 +1,8 @@ import express from "express"; import eventRouter from "./event.routes"; import { - addVisual, createDiscipline, deleteDiscipline, - deleteVisual, getAllDisciplines, getDiscipline, updateDiscipline, @@ -20,18 +18,6 @@ router.get("/:pid", getDiscipline); router.patch("/:pid", requireAuthentication, updateDiscipline); router.delete<"/:pid", { pid: string }>("/:pid", requireAuthentication, deleteDiscipline); -router.post<"/:disciplinePid/images", { disciplinePid: string }>( - "/:disciplinePid/images", - requireAuthentication, - addVisual -); - -router.delete<"/:disciplinePid/images/:pid", { disciplinePid: string; pid: string }>( - "/:disciplinePid/images/:pid", - requireAuthentication, - deleteVisual -); - eventRouter.post("/:eventPid/disciplines", requireAuthentication, createDiscipline); export default router; diff --git a/src/Routes/event.routes.ts b/src/Routes/event.routes.ts index 5e68bd8..7c2d34d 100644 --- a/src/Routes/event.routes.ts +++ b/src/Routes/event.routes.ts @@ -2,9 +2,7 @@ import Express from "express"; import { string } from "zod"; import { addEvent, - addVisual, deleteEvent, - deleteVisual, getAllEvents, getEvent, updateEvent, @@ -22,12 +20,4 @@ router.patch<"/:pid/", { pid: string }>("/:pid/", requireAuthentication, updateE router.delete<"/:pid/", { pid: string }>("/:pid/", requireAuthentication, deleteEvent); -router.post<"/:eventPid/media", { eventPid: string }>("/:eventPid/media", requireAuthentication, addVisual); - -router.delete<"/:eventPid/media/:pid", { eventPid: string; pid: string }>( - "/:eventPid/media/:pid", - requireAuthentication, - deleteVisual -); - export default router; diff --git a/src/Routes/media.routes.ts b/src/Routes/media.routes.ts index f340de1..e4c633a 100644 --- a/src/Routes/media.routes.ts +++ b/src/Routes/media.routes.ts @@ -1,7 +1,7 @@ import express from "express"; import fileUpload from "express-fileupload"; import { requireAuthentication } from "../Middleware/auth/auth"; -import { deleteMedia, getAllMedia, getMediaMeta, uploadImage } from "../Controllers/media.controller"; +import { deleteMedia, getAllMedia, getMediaMeta, linkMedia, unlinkMedia, uploadImage } from "../Controllers/media.controller"; import eventRouter from "./event.routes"; import disciplineRouter from "./discipline.routes"; import roleSchemaRouter from "./role_schema.routes"; @@ -19,4 +19,28 @@ router.get("/:pid/meta", getMediaMeta); router.delete("/:pid", requireAuthentication, deleteMedia); +eventRouter.post<"/:pid/media", { pid: string }>( + "/:pid/media", requireAuthentication, linkMedia +); + +eventRouter.delete<"/:pid/media/:mediaPid", { pid: string, mediaPid: string }>( + "/:pid/media/:mediaPid", requireAuthentication, unlinkMedia +); + +disciplineRouter.post<"/:pid/media", { pid: string }>( + "/:pid/media", requireAuthentication, linkMedia +); + +disciplineRouter.delete<"/:pid/media/:mediaPid", { pid: string, mediaPid: string }>( + "/:pid/media/:mediaPid", requireAuthentication, unlinkMedia +); + +roleSchemaRouter.post<"/:pid/media", { pid: string }>( + "/:pid/media", requireAuthentication, linkMedia +); + +roleSchemaRouter.delete<"/:pid/media/:mediaPid", { pid: string, mediaPid: string }>( + "/:pid/media/:mediaPid", requireAuthentication, unlinkMedia +); + export default router; diff --git a/src/Routes/role_schema.routes.ts b/src/Routes/role_schema.routes.ts index f6ada5f..688dafc 100644 --- a/src/Routes/role_schema.routes.ts +++ b/src/Routes/role_schema.routes.ts @@ -1,9 +1,7 @@ import express from "express"; import disciplineRouter from "./discipline.routes"; import { - addVisual, createRoleSchema, - deleteVisual, getAllRoleSchemas, getAllRoleSchemasWithParam, getRoleSchema, @@ -20,12 +18,4 @@ disciplineRouter.get("/:disciplinePid/role-schemas", getAllRoleSchemasWithParam) disciplineRouter.post("/:disciplinePid/role-schemas", requireAuthentication, createRoleSchema); -router.post<"/:schemaPid/images", { schemaPid: string }>("/:schemaPid/images", requireAuthentication, addVisual); - -router.delete<"/:schemaPid/images/:pid", { schemaPid: string; pid: string }>( - "/:schemaPid/images/:pid", - requireAuthentication, - deleteVisual -); - export default router; diff --git a/src/app.ts b/src/app.ts index 7a41c6c..9121346 100644 --- a/src/app.ts +++ b/src/app.ts @@ -95,9 +95,6 @@ async function main() { // Error handling app.use(defaultErrorHandler); // This has to be the LAST ROUTE - // Disable the media router for now - // app.use("/api/media", mediaRouter); - app.use(notFoundHandler); app.listen(process.env.PORT, () => {