From df9b47dbfda5d33159648264976b5602e1a006a0 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Thu, 28 Apr 2022 15:04:39 +0200 Subject: [PATCH 1/6] Add router to delete groups + Add route to delete organisations --- src/Controllers/group.controllers.ts | 28 ++++++++++++++++++++++++++-- src/Routes/group.routes.ts | 9 ++++++++- src/Routes/organisation.routes.ts | 2 ++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/Controllers/group.controllers.ts b/src/Controllers/group.controllers.ts index 10f42c7..ee7fb5e 100644 --- a/src/Controllers/group.controllers.ts +++ b/src/Controllers/group.controllers.ts @@ -1,8 +1,8 @@ import { Admin } from "@prisma/client"; -import { PrismaClientUnknownRequestError } from "@prisma/client/runtime"; +import { PrismaClientKnownRequestError, PrismaClientUnknownRequestError } from "@prisma/client/runtime"; import { Request, Response } from "express"; import prisma from "../lib/prisma"; -import { generateError, genericError, handleCreateByName } from "./common"; +import { createInsufficientPermissionsError, generateError, genericError, handleCreateByName } from "./common"; const basicGroup = { pid: true, @@ -114,3 +114,27 @@ export const createGroup = async (req: Request<{ organisationPid: string }, {}, res ); }; + +interface DeleteGroupQueryParams { + pid: string; +} + +export const deleteGroup = async (req: Request, res: Response) => { + if (req.auth?.permission_level !== "ELEVATED") { + return res.status(403).json(createInsufficientPermissionsError()); + } + + const { pid } = req.params; + + try { + prisma.group.delete({ where: { pid } }); + + return res.status(204).end(); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + return res.status(404).json(generateError(`The group with the ID ${pid} could not be found`)); + } + + throw e; + } +}; diff --git a/src/Routes/group.routes.ts b/src/Routes/group.routes.ts index 12ae59d..4ab9387 100644 --- a/src/Routes/group.routes.ts +++ b/src/Routes/group.routes.ts @@ -1,5 +1,11 @@ import express from "express"; -import { createGroup, getAllGroups, getAllGroupsWithParam, getGroup } from "../Controllers/group.controllers"; +import { + createGroup, + deleteGroup, + getAllGroups, + getAllGroupsWithParam, + getGroup, +} from "../Controllers/group.controllers"; import { requireAuthentication } from "../Middleware/auth/auth"; import organisationRouter from "./organisation.routes"; @@ -7,6 +13,7 @@ const router = express.Router(); router.get("/", getAllGroups); router.get("/:pid", getGroup); +router.delete<"/:pid", { pid: string }>("/:pid", requireAuthentication, deleteGroup); organisationRouter.get("/:organisationPid/groups", getAllGroupsWithParam); organisationRouter.post("/:organisationPid/groups", requireAuthentication, createGroup); diff --git a/src/Routes/organisation.routes.ts b/src/Routes/organisation.routes.ts index 7cff08d..9cda611 100644 --- a/src/Routes/organisation.routes.ts +++ b/src/Routes/organisation.routes.ts @@ -2,6 +2,7 @@ import express from "express"; import eventRouter from "./event.routes"; import { createOrganisation, + deleteOrganisation, getAllOrganisations, getAllOrganisationsWithParam, getOrganisation, @@ -14,6 +15,7 @@ const router = express.Router(); router.get("/", getAllOrganisations); router.get("/:pid", getOrganisation); router.put<"/:pid", { pid: string }>("/:pid", requireAuthentication, updateOrganisation); +router.delete<"/:pid", { pid: string }>("/:pid", requireAuthentication, deleteOrganisation); eventRouter.get("/:eventPid/organisations", getAllOrganisationsWithParam); eventRouter.post<"/:eventPid/organisations", { eventPid: string }>( From 3c8cea8c7145b627432592ee87fd49ea2d7129e8 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Thu, 28 Apr 2022 16:14:48 +0200 Subject: [PATCH 2/6] Add routes for getting disciplines + Throwing an error still crashes the server --- src/Controllers/discipline.controller.ts | 102 +++++++++++++++++++++++ src/Routes/discipline.routes.ts | 9 ++ src/app.ts | 5 +- 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 src/Controllers/discipline.controller.ts create mode 100644 src/Routes/discipline.routes.ts diff --git a/src/Controllers/discipline.controller.ts b/src/Controllers/discipline.controller.ts new file mode 100644 index 0000000..8a927a1 --- /dev/null +++ b/src/Controllers/discipline.controller.ts @@ -0,0 +1,102 @@ +import { Prisma, Organisation, Admin, AdminLevel, Team } from "@prisma/client"; +import { Request, Response } from "express"; +import prisma from "../lib/prisma"; +import ForwardableError from "../Middleware/error/ForwardableError"; + +const basicDiscipline = { + pid: true, + name: true, + visual: { select: { pid: true, location: true } }, + maxTeamSize: true, + minTeamSize: true, + event: { select: { pid: true, name: true } }, + roles: { select: { pid: true, name: true } }, +} as const; + +const authenticatedDiscipline = { + ...basicDiscipline, + teams: { select: { pid: true, name: true } }, +} as const; + +const elevatedDiscipline: Prisma.DisciplineFindManyArgs["select"] = { + ...authenticatedDiscipline, + teams: { select: { pid: true, name: true, leaderEmail: true } }, +}; + +export const _getAllDisciplines = async ( + res: Response, + authenticated: boolean | undefined, + eventId: string | undefined +) => { + const disciplines = await prisma.discipline.findMany({ + where: { event: { pid: eventId } }, + select: !authenticated ? basicDiscipline : authenticatedDiscipline, + }); + + return res.status(200).json({ + type: "success", + payload: { + disciplines, + }, + }); +}; + +interface GetAllDisciplinesSearchParams { + eventPid?: string; +} + +export const getAllDisciplines = async (req: Request<{}, {}, {}, GetAllDisciplinesSearchParams>, res: Response) => { + return _getAllDisciplines(res, req.auth?.isAuthenticated, req.query.eventPid); +}; + +interface GetAllDisciplinesQueryParams { + eventPid: string; +} + +export const GetAllDisciplinesWithParam = async (req: Request, res: Response) => { + return _getAllDisciplines(res, req.auth?.isAuthenticated, req.params.eventPid); +}; + +interface GetDisciplineQueryParams { + pid: string; +} + +export const getDiscipline = async (req: Request, res: Response) => { + const { pid } = req.params; + + const discipline = await prisma.discipline.findUnique({ + where: { pid }, + select: !req.auth?.isAuthenticated + ? basicDiscipline + : req.auth.permission_level !== "ELEVATED" + ? authenticatedDiscipline + : elevatedDiscipline, + }); + + if (!discipline) { + throw new ForwardableError(404, `The discipline with ID ${pid} could not be found`); + } + + return res.status(200).json({ + type: "success", + payload: { + discipline: { + ...discipline, + event: { + ...discipline.event, + _links: [{ rel: "self", type: "GET", href: `/api/events/${discipline.event.pid}` }], + }, + roles: discipline.roles.map((role) => ({ + ...role, + _links: [{ rel: "self", type: "GET", href: `/api/role/${role.pid}` }], + })), + ...((discipline as any).teams + ? (discipline as any).teams.map((team: Team) => ({ + ...team, + _links: [{ rel: "self", type: "GET", href: `/api/teams/${team.pid}` }], + })) + : {}), + }, + }, + }); +}; diff --git a/src/Routes/discipline.routes.ts b/src/Routes/discipline.routes.ts new file mode 100644 index 0000000..ef158cd --- /dev/null +++ b/src/Routes/discipline.routes.ts @@ -0,0 +1,9 @@ +import express from "express"; +import { getAllDisciplines, getDiscipline } from "../Controllers/discipline.controller"; + +const router = express.Router(); + +router.get("/", getAllDisciplines); // TODO: Optional auth +router.get("/:pid", getDiscipline); + +export default router; diff --git a/src/app.ts b/src/app.ts index da3df2c..64e9789 100644 --- a/src/app.ts +++ b/src/app.ts @@ -6,6 +6,7 @@ import argon2 from "argon2"; import adminRouter from "./Routes/admin.routes"; import organisationRouter from "./Routes/organisation.routes"; import groupRouter from "./Routes/group.routes"; +import disciplineRouter from "./Routes/discipline.routes"; import defaultErrorHandler from "./Middleware/error/handler"; import logger from "./Middleware/error/logger"; import debugLogger from "./Middleware/debug/logger"; @@ -53,8 +54,10 @@ async function main() { app.use("/api/groups", groupRouter); + app.use("/api/disciplines", disciplineRouter); + // Error handling - app.use(defaultErrorHandler); + app.use(defaultErrorHandler); // Not working app.listen(process.env.PORT, () => { logger.info(`Listening on port ${process.env.PORT}`); From 9bafdebd31474d410adf9f3bba8a07d9103f4e09 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Fri, 29 Apr 2022 10:08:50 +0200 Subject: [PATCH 3/6] Add function to create disciplines + Fix async errors + Extract name error --- src/Controllers/common.ts | 14 +++--- src/Controllers/discipline.controller.ts | 64 +++++++++++++++++++++++- src/Middleware/error/ForwardableError.ts | 1 + src/Middleware/error/NotFoundError.ts | 13 +++++ src/Routes/discipline.routes.ts | 6 ++- 5 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 src/Middleware/error/NotFoundError.ts diff --git a/src/Controllers/common.ts b/src/Controllers/common.ts index a9a0caf..88fc839 100644 --- a/src/Controllers/common.ts +++ b/src/Controllers/common.ts @@ -64,6 +64,13 @@ export const genericError = { }, }; +export const NAME_ERROR = { + type: "error", + payload: { + message: "The name has to be at least 1 character long", + }, +}; + export function validateName(name: string) { return name.length > 0; } @@ -106,12 +113,7 @@ export async function handleCreateByName( } if (!validateName(name)) { - return res.status(400).json({ - type: "error", - payload: { - message: "The name has to be at least 1 character long", - }, - }); + return res.status(400).json(NAME_ERROR); } // Check if link object exsits diff --git a/src/Controllers/discipline.controller.ts b/src/Controllers/discipline.controller.ts index 8a927a1..fbe4a72 100644 --- a/src/Controllers/discipline.controller.ts +++ b/src/Controllers/discipline.controller.ts @@ -1,7 +1,19 @@ import { Prisma, Organisation, Admin, AdminLevel, Team } from "@prisma/client"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; import { Request, Response } from "express"; import prisma from "../lib/prisma"; import ForwardableError from "../Middleware/error/ForwardableError"; +import NotFoundError from "../Middleware/error/NotFoundError"; +import { + createInsufficientPermissionsError, + DataType, + generateError, + generateInvalidBodyError, + NAME_ERROR, + validateName, +} from "./common"; + +require("express-async-errors"); const basicDiscipline = { pid: true, @@ -74,7 +86,7 @@ export const getDiscipline = async (req: Request, res: }); if (!discipline) { - throw new ForwardableError(404, `The discipline with ID ${pid} could not be found`); + throw new NotFoundError("discipline", pid); } return res.status(200).json({ @@ -100,3 +112,53 @@ export const getDiscipline = async (req: Request, res: }, }); }; + +interface CreateDisciplineBody { + name?: string; + minTeamSize?: number; + maxTeamSize?: number; +} + +// require: auth(ELEVATED) +// at: POST /event/:eventPid/discipliens +export const createDiscipline = async (req: Request<{ eventPid: string }, {}, CreateDisciplineBody>, res: Response) => { + if (req.auth?.permission_level !== "ELEVATED") { + return res.status(403).json(createInsufficientPermissionsError()); + } + + const { name, minTeamSize, maxTeamSize } = req.body; + + if (typeof name !== "string" || typeof minTeamSize !== "number" || typeof maxTeamSize !== "number") { + return res.status(400).json( + generateInvalidBodyError({ + name: DataType.STRING, + minTeamSize: DataType.NUMBER, + maxTeamSize: DataType.NUMBER, + }) + ); + } + + if (!validateName(name)) { + return res.status(400).json(NAME_ERROR); + } + + try { + const discipline = await prisma.discipline.create({ + data: { name, minTeamSize, maxTeamSize, event: { connect: { pid: req.params.eventPid } } }, + select: { + pid: true, + name: true, + minTeamSize: true, + maxTeamSize: true, + event: { select: { pid: true, name: true } }, + }, + }); + + return res.status(201).json({ type: "success", payload: { discipline } }); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + return res.status(404).json(generateError(`Could not link to event with ID '${req.params.eventPid}'`)); + } + throw e; + } +}; diff --git a/src/Middleware/error/ForwardableError.ts b/src/Middleware/error/ForwardableError.ts index 04bc5e1..bbae485 100644 --- a/src/Middleware/error/ForwardableError.ts +++ b/src/Middleware/error/ForwardableError.ts @@ -1,6 +1,7 @@ export default class ForwardableError extends Error { // If in different context private readonly __id = "CUSTOM_ERROR"; + protected readonly __oid?: string; public readonly status: number; diff --git a/src/Middleware/error/NotFoundError.ts b/src/Middleware/error/NotFoundError.ts new file mode 100644 index 0000000..382441f --- /dev/null +++ b/src/Middleware/error/NotFoundError.ts @@ -0,0 +1,13 @@ +import ForwardableError from "./ForwardableError"; + +export default class NotFoundError extends ForwardableError { + protected __oid = "NOT_FOUND_ERROR"; + + constructor(resource?: string, pid?: string) { + super(404, `The requested ${resource ?? "resource"}${pid ? ` with PID '${pid}'` : ""} could not be found!`); + } + + static isNotFoundError(err: any): err is NotFoundError { + return err.__oid === "NOT_FOUND_ERROR"; + } +} diff --git a/src/Routes/discipline.routes.ts b/src/Routes/discipline.routes.ts index ef158cd..3ae7ad1 100644 --- a/src/Routes/discipline.routes.ts +++ b/src/Routes/discipline.routes.ts @@ -1,9 +1,13 @@ import express from "express"; -import { getAllDisciplines, getDiscipline } from "../Controllers/discipline.controller"; +import eventRouter from "./event.routes"; +import { createDiscipline, getAllDisciplines, getDiscipline } from "../Controllers/discipline.controller"; +import { requireAuthentication } from "../Middleware/auth/auth"; const router = express.Router(); router.get("/", getAllDisciplines); // TODO: Optional auth router.get("/:pid", getDiscipline); +eventRouter.post("/:eventPid/disciplines", requireAuthentication, createDiscipline); + export default router; From 2034faa4483d59f5c27979f25f19ff11371deba2 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Sun, 1 May 2022 16:09:10 +0200 Subject: [PATCH 4/6] Add endpoint to delete disciplines --- src/Controllers/discipline.controller.ts | 27 +++++++++++++++++++++++- src/Routes/discipline.routes.ts | 8 ++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/Controllers/discipline.controller.ts b/src/Controllers/discipline.controller.ts index fbe4a72..93431ae 100644 --- a/src/Controllers/discipline.controller.ts +++ b/src/Controllers/discipline.controller.ts @@ -1,5 +1,5 @@ import { Prisma, Organisation, Admin, AdminLevel, Team } from "@prisma/client"; -import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; +import { PrismaClientKnownRequestError, PrismaClientUnknownRequestError } from "@prisma/client/runtime"; import { Request, Response } from "express"; import prisma from "../lib/prisma"; import ForwardableError from "../Middleware/error/ForwardableError"; @@ -162,3 +162,28 @@ export const createDiscipline = async (req: Request<{ eventPid: string }, {}, Cr throw e; } }; + +interface DeleteDisciplineQueryParams { + pid: string; +} + +// requires: auth(ELEVATED) +export const deleteDiscipline = async (req: Request, res: Response) => { + if (req.auth?.permission_level !== "ELEVATED") { + res.status(403).json(createInsufficientPermissionsError()); + } + + const { pid } = req.params; + + try { + await prisma.discipline.delete({ where: { pid } }); + + return res.status(204).end(); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError("discipline", pid); + } + + throw e; + } +}; diff --git a/src/Routes/discipline.routes.ts b/src/Routes/discipline.routes.ts index 3ae7ad1..08aec30 100644 --- a/src/Routes/discipline.routes.ts +++ b/src/Routes/discipline.routes.ts @@ -1,12 +1,18 @@ import express from "express"; import eventRouter from "./event.routes"; -import { createDiscipline, getAllDisciplines, getDiscipline } from "../Controllers/discipline.controller"; +import { + createDiscipline, + deleteDiscipline, + getAllDisciplines, + getDiscipline, +} from "../Controllers/discipline.controller"; import { requireAuthentication } from "../Middleware/auth/auth"; const router = express.Router(); router.get("/", getAllDisciplines); // TODO: Optional auth router.get("/:pid", getDiscipline); +router.delete<"/:pid", { pid: string }>("/:pid", requireAuthentication, deleteDiscipline); eventRouter.post("/:eventPid/disciplines", requireAuthentication, createDiscipline); From 72ee918f00b9cad1dad2e2ef5f34593121974b53 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Sun, 1 May 2022 17:18:55 +0200 Subject: [PATCH 5/6] Add schema valiation + Using zod for validation + Add SchemaError --- package.json | 3 +- src/Middleware/error/SchemaError.ts | 9 ++++ src/lib/result_schema.ts | 65 +++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 src/Middleware/error/SchemaError.ts create mode 100644 src/lib/result_schema.ts diff --git a/package.json b/package.json index e51d789..850d786 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "jsonwebtoken": "^8.5.1", "nodemailer": "^6.7.0", "redis": "^3.1.2", - "winston": "^3.7.2" + "winston": "^3.7.2", + "zod": "^3.14.4" }, "devDependencies": { "@types/chai": "^4.2.22", diff --git a/src/Middleware/error/SchemaError.ts b/src/Middleware/error/SchemaError.ts new file mode 100644 index 0000000..6d4026d --- /dev/null +++ b/src/Middleware/error/SchemaError.ts @@ -0,0 +1,9 @@ +import ForwardableError from "./ForwardableError"; + +export default class SchemaError extends ForwardableError { + protected __oid = "SCHEMA_ERROR"; + + constructor(message: string) { + super(400, message); + } +} diff --git a/src/lib/result_schema.ts b/src/lib/result_schema.ts new file mode 100644 index 0000000..53bcc38 --- /dev/null +++ b/src/lib/result_schema.ts @@ -0,0 +1,65 @@ +import { Schema, z, ZodError } from "zod"; +import SchemaError from "../Middleware/error/SchemaError"; + +// -- Schema definitions -- + +const SchemaVersion = z.enum(["1.0"]); + +const TimeUnit = z.enum(["days", "hours", "minutes", "seconds", "milliseconds"]); + +const DurationSchema = z + .object({ + type: z.literal("duration"), + min: z.number().int({ message: "min must be an integer (relative to smallestUnit)" }), + max: z.number().int({ message: "max must be an integer (relative to smallestUnit)" }), + smallestUnit: TimeUnit, + higherIsBetter: z.boolean(), + }) + .refine(({ min, max }) => min < max, { message: "min must be smaller than max" }); + +const PointSchema = z + .object({ + type: z.literal("points"), + min: z.number(), + max: z.number(), + step: z.number(), + start: z.number(), + + unit: z.string(), + unitSign: z.string(), + + higherIsBetter: z.boolean(), + }) + .refine(({ min, max }) => min < max, { message: "min must be smaller than max" }) + .refine(({ min, max, start }) => min <= start && max >= start, { + message: "start must be larger than or equal to min and smaller than or equal to max", + }); + +export type DurationSchemaT = z.infer; +export type PointSchemaT = z.infer; + +// -- Parser -- + +export function parseSchema(schema: any): DurationSchemaT | PointSchemaT { + try { + if (schema.type === "duration") { + return DurationSchema.parse(schema); + } else if (schema.type === "points") { + return PointSchema.parse(schema); + } + } catch (e) { + if (e instanceof ZodError) { + const issue = e.issues.at(0); + + if (issue) { + throw new SchemaError(`${issue.path}: ${issue.message}`); + } + + throw new SchemaError("Unknown error occured while validating the schema"); + } + + throw e; + } + + throw new SchemaError("type must be either 'duration' or 'points'"); +} From 8384a3eebbd0df0bc55fb4ce4b155f67ac51b1cc Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Sun, 1 May 2022 18:18:48 +0200 Subject: [PATCH 6/6] Add controller for roleSchemas (unfinished) + Refine error messages for role schemas + Add role schema routes + Add RESULT_SCHEMA DataType + Add SchemaError.isSchemaError() type guard --- src/Controllers/common.ts | 1 + src/Controllers/role_schema.controller.ts | 125 ++++++++++++++++++++++ src/Middleware/error/SchemaError.ts | 4 + src/Routes/role_schema.routes.ts | 19 ++++ src/app.ts | 3 + src/lib/result_schema.ts | 4 +- 6 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 src/Controllers/role_schema.controller.ts create mode 100644 src/Routes/role_schema.routes.ts diff --git a/src/Controllers/common.ts b/src/Controllers/common.ts index 88fc839..ad2dc49 100644 --- a/src/Controllers/common.ts +++ b/src/Controllers/common.ts @@ -10,6 +10,7 @@ export enum DataType { PERMISSION_LEVEL = "'ELEVATED' | 'STANDARD'", DATETIME = "ISOstring", UUID = "string", + RESULT_SCHEMA = "result_schema", } interface Body { diff --git a/src/Controllers/role_schema.controller.ts b/src/Controllers/role_schema.controller.ts new file mode 100644 index 0000000..ca73554 --- /dev/null +++ b/src/Controllers/role_schema.controller.ts @@ -0,0 +1,125 @@ +import { Prisma } from "@prisma/client"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; +import { Request, Response } from "express"; +import prisma from "../lib/prisma"; +import { DurationSchemaT, parseSchema, PointSchemaT } from "../lib/result_schema"; +import NotFoundError from "../Middleware/error/NotFoundError"; +import SchemaError from "../Middleware/error/SchemaError"; +import { + createInsufficientPermissionsError, + DataType, + generateInvalidBodyError, + NAME_ERROR, + validateName, +} from "./common"; + +const roleSchema = { + name: true, + schema: true, + discipline: { select: { pid: true, name: true } }, + visual: { select: { pid: true, location: true } }, +} as const; + +export const _getAllRoleSchemas = async (res: Response, disciplinePid: string | undefined) => { + const schemas = await prisma.roleSchema.findMany({ + where: { discipline: { pid: disciplinePid } }, + select: roleSchema, + }); + + return res.status(200).json({ + type: "success", + payload: { + roleSchemas: schemas, + }, + }); +}; + +interface GetAllRoleSchemasSearchParams { + disciplinePid?: string; +} + +export const getAllRoleSchemas = async (req: Request<{}, {}, {}, GetAllRoleSchemasSearchParams>, res: Response) => { + return _getAllRoleSchemas(res, req.query.disciplinePid); +}; + +interface GetAllRoleSchemasWithParamQueryParams { + organisationPid: string; +} + +export const getAllRoleSchemasWithParam = async ( + req: Request, + res: Response +) => { + return _getAllRoleSchemas(res, req.params.organisationPid); +}; + +interface GetRoleSchemaQueryParams { + pid: string; +} + +export const getRoleSchema = async (req: Request, res: Response) => { + const { pid } = req.params; + + const schema = await prisma.roleSchema.findUnique({ where: { pid }, select: roleSchema }); + + if (!schema) { + throw new NotFoundError("roleSchema", pid); + } + + return res.status(200).json({ + type: "success", + payload: { + roleSchema: { + ...schema, + discipline: { + ...schema.discipline, + _links: [{ rel: "self", type: "GET", href: `/api/disciplines/${schema.discipline.pid}` }], + }, + }, + }, + }); +}; + +interface CreateRoleSchemaBody { + name?: string; + schema?: any; +} + +// at: POST /discipline/:disciplinePid/role-schemas +// requires: auth(ELEVATED) +export const createRoleSchema = async ( + req: Request<{ disciplinePid: string }, {}, CreateRoleSchemaBody>, + res: Response +) => { + if (req.auth?.permission_level !== "ELEVATED") { + return res.status(403).json(createInsufficientPermissionsError()); + } + + const { name, schema: resultSchema } = req.body; + + if (typeof name !== "string") { + return res.status(400).json(generateInvalidBodyError({ name: DataType.STRING, schema: DataType.RESULT_SCHEMA })); + } + + if (!validateName(name)) { + return res.status(400).json(NAME_ERROR); + } + + // Validate the result schema (Errors should be handled by the default error handler) + const validatedSchema = parseSchema(resultSchema); + + try { + const schema = await prisma.roleSchema.create({ + data: { name, schema: validatedSchema, discipline: { connect: { pid: req.params.disciplinePid } } }, + select: roleSchema, + }); + + return res.status(201).json({ type: "success", payload: { roleSchema: schema } }); + } catch (e) { + if (e instanceof PrismaClientKnownRequestError && e.code === "P2025") { + throw new NotFoundError("discipline", req.params.disciplinePid); + } + + throw e; + } +}; diff --git a/src/Middleware/error/SchemaError.ts b/src/Middleware/error/SchemaError.ts index 6d4026d..b880480 100644 --- a/src/Middleware/error/SchemaError.ts +++ b/src/Middleware/error/SchemaError.ts @@ -6,4 +6,8 @@ export default class SchemaError extends ForwardableError { constructor(message: string) { super(400, message); } + + public static isSchemaError(err: any): err is SchemaError { + return err.__oid === "SCHEMA_ERROR"; + } } diff --git a/src/Routes/role_schema.routes.ts b/src/Routes/role_schema.routes.ts new file mode 100644 index 0000000..ea3c938 --- /dev/null +++ b/src/Routes/role_schema.routes.ts @@ -0,0 +1,19 @@ +import express from "express"; +import disciplineRouter from "./discipline.routes"; +import { + createRoleSchema, + getAllRoleSchemas, + getAllRoleSchemasWithParam, + getRoleSchema, +} from "../Controllers/role_schema.controller"; +import { requireAuthentication } from "../Middleware/auth/auth"; + +const router = express.Router(); + +router.get("/", getAllRoleSchemas); +router.get("/:pid", getRoleSchema); + +disciplineRouter.get("/:disciplinePid/role-schemas", getAllRoleSchemasWithParam); +disciplineRouter.post("/:disciplinePid/role-schemas", requireAuthentication, createRoleSchema); + +export default router; diff --git a/src/app.ts b/src/app.ts index 64e9789..39dcc0f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -7,6 +7,7 @@ import adminRouter from "./Routes/admin.routes"; import organisationRouter from "./Routes/organisation.routes"; import groupRouter from "./Routes/group.routes"; import disciplineRouter from "./Routes/discipline.routes"; +import roleSchemaRouter from "./Routes/role_schema.routes"; import defaultErrorHandler from "./Middleware/error/handler"; import logger from "./Middleware/error/logger"; import debugLogger from "./Middleware/debug/logger"; @@ -56,6 +57,8 @@ async function main() { app.use("/api/disciplines", disciplineRouter); + app.use("/api/role-schemas", roleSchemaRouter); + // Error handling app.use(defaultErrorHandler); // Not working diff --git a/src/lib/result_schema.ts b/src/lib/result_schema.ts index 53bcc38..7da23c9 100644 --- a/src/lib/result_schema.ts +++ b/src/lib/result_schema.ts @@ -52,7 +52,7 @@ export function parseSchema(schema: any): DurationSchemaT | PointSchemaT { const issue = e.issues.at(0); if (issue) { - throw new SchemaError(`${issue.path}: ${issue.message}`); + throw new SchemaError(`Error with the schema: ${issue.path ? `${issue.path}:` : ""} ${issue.message}`); } throw new SchemaError("Unknown error occured while validating the schema"); @@ -61,5 +61,5 @@ export function parseSchema(schema: any): DurationSchemaT | PointSchemaT { throw e; } - throw new SchemaError("type must be either 'duration' or 'points'"); + throw new SchemaError("Error with the schema: type must be either 'duration' or 'points'"); }