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] 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'"); }