diff --git a/dbschema/default.esdl b/dbschema/default.esdl index 1d64b6d..4430594 100644 --- a/dbschema/default.esdl +++ b/dbschema/default.esdl @@ -22,6 +22,7 @@ module default { }; classes := . ({ diff --git a/src/routes/assignments/create.ts b/src/routes/assignments/create.ts new file mode 100644 index 0000000..ac7875b --- /dev/null +++ b/src/routes/assignments/create.ts @@ -0,0 +1,124 @@ +import e from "@edgedb"; +import { + DATABASE_READ_FAILED, + DATABASE_WRITE_FAILED, + FORBIDDEN, + UNAUTHORIZED, +} from "constants/responses"; +import Elysia, { t } from "elysia"; +import { HttpStatusCode } from "elysia-http-status-code"; +import { client } from "index"; +import { auth } from "plugins/auth"; +import { customDateToNormal } from "utils/dates/customAndNormal"; +import { doesClassExist } from "utils/db/classes"; +import { promiseResult } from "utils/errors"; +import { responseBuilder } from "utils/response"; + +export const createAssignment = new Elysia() + .use(HttpStatusCode()) + .use(auth) + .post( + "/", + async ({ auth, set, httpStatus, body }) => { + if (!auth.isAuthorized) { + set.status = httpStatus.HTTP_401_UNAUTHORIZED; + return UNAUTHORIZED; + } + + const from = customDateToNormal(body.from) + const due = customDateToNormal(body.due) + if (due < from) { + set.status = httpStatus.HTTP_400_BAD_REQUEST; + return responseBuilder('error', { error: "Due must not be earlier then from" }); + } + + + const classExists = await promiseResult(() => doesClassExist({ + schoolName: body.school, + className: body.class + })) + if (classExists.isError) { + set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR + return DATABASE_READ_FAILED + } + if (!classExists.data) { + set.status = httpStatus.HTTP_404_NOT_FOUND; + return responseBuilder('error', { + error: "Can't find that school or class" + }) + } + + + const isUserInClassQuery = e.count( + e.select(e.Class, (c) => { + const classNameMatches = e.op(c.name, "=", body.class); + const schoolMatches = e.op( + classNameMatches, + "and", + e.op(c.school.name, "=", body.school), + ); + const userMatches = e.op(c.students.username, "=", auth.username); + + return { filter_single: e.op(schoolMatches, "and", userMatches) }; + }), + ); + const isUserInClassResult = await promiseResult(() => + isUserInClassQuery.run(client), + ); + if (isUserInClassResult.isError) { + set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR; + return DATABASE_READ_FAILED; + } + if (isUserInClassResult.data === 0) { + set.status = httpStatus.HTTP_403_FORBIDDEN; + return FORBIDDEN; + } + + const insertQuery = e.insert(e.Assignment, { + subject: body.subject, + description: body.description, + fromDate: from, + dueDate: due, + class: e.select(e.Class, (c) => { + const classNameMatches = e.op(c.name, "=", body.class); + const schoolMatches = e.op(c.school.name, "=", body.school); + return { + filter_single: e.op(classNameMatches, "and", schoolMatches), + }; + }), + updatedBy: e.select(e.User, (u) => ({ + filter_single: e.op(u.username, "=", auth.username), + })), + }); + const insertResult = await promiseResult(() => insertQuery.run(client)); + if (insertResult.isError) { + set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR; + return DATABASE_WRITE_FAILED; + } + + set.status = httpStatus.HTTP_201_CREATED; + return responseBuilder("success", { + message: "Successfully created assignment", + data: insertResult.data + }); + }, + { + body: t.Object({ + school: t.String({ minLength: 1 }), + class: t.String({ minLength: 1 }), + + subject: t.String({ minLength: 1 }), + description: t.String({ minLength: 1 }), + from: t.Object( { + day: t.Number({ minimum: 1, maximum: 31 }), + month: t.Number({ minimum: 1, maximum: 12 }), + year: t.Number({ minimum: 1970 }), + }), + due: t.Object({ + day: t.Number({ minimum: 1, maximum: 31 }), + month: t.Number({ minimum: 1, maximum: 12 }), + year: t.Number({ minimum: 1970 }), + }), + }), + }, + ); diff --git a/src/routes/assignments/index.ts b/src/routes/assignments/index.ts new file mode 100644 index 0000000..9ad6b31 --- /dev/null +++ b/src/routes/assignments/index.ts @@ -0,0 +1,6 @@ +import Elysia from "elysia"; +import { createAssignment } from "./create"; + +export const assignmentsRouter = new Elysia({ prefix: "/assignments" }).use( + createAssignment, +); diff --git a/src/types/date.ts b/src/types/date.ts new file mode 100644 index 0000000..d66dc88 --- /dev/null +++ b/src/types/date.ts @@ -0,0 +1,5 @@ +export type CustomDate = { + day: number; + month: number; + year: number; +}; diff --git a/src/utils/dates/current.ts b/src/utils/dates/current.ts new file mode 100644 index 0000000..579f38c --- /dev/null +++ b/src/utils/dates/current.ts @@ -0,0 +1,27 @@ +/** + * Returns the current day + * Ranging from 1 to 31 (inclusive) + */ +export const currentDay = () => new Date().getDate(); + +/** + * Returns the current month + * Ranging from 1 to 12 (inclusive) + */ +export const currentMonth = () => new Date().getMonth() + 1; + +/** + * Returns the current year + * e.g. 2021 + */ +export const currentYear = () => new Date().getFullYear(); + +/** + * Returns the current date + * e.g. { day: 1, month: 1, year: 2021 } + */ +export const currentCustomDate = () => ({ + day: currentDay(), + month: currentMonth(), + year: currentYear(), +}); diff --git a/src/utils/dates/customAndNormal.ts b/src/utils/dates/customAndNormal.ts new file mode 100644 index 0000000..3a9f69a --- /dev/null +++ b/src/utils/dates/customAndNormal.ts @@ -0,0 +1,19 @@ +import type { CustomDate } from "types/date"; + +/** + * Converts a custom date record to a normal js date + */ +export const customDateToNormal = (customDate: CustomDate) => { + return new Date(customDate.year, customDate.month - 1, customDate.day); +}; + +/** + * Converts a normal js date to a custom date record + */ +export const normalDateToCustom = (date: Date) => { + const day = date.getDate(); + const month = date.getMonth() + 1; + const year = date.getFullYear(); + + return { day, month, year }; +}; diff --git a/src/utils/db/classes.ts b/src/utils/db/classes.ts index c5f2e98..2a8d764 100644 --- a/src/utils/db/classes.ts +++ b/src/utils/db/classes.ts @@ -2,16 +2,18 @@ import e from "@edgedb"; import { client } from "index"; import { promiseResult } from "utils/errors"; +interface ClassIdentifier { + className: string; + schoolName: string; +} + /** * Get the amount of members in a class * This only gives the accepted members - requests are ignored * It doesn't modify the database but reads from it * @throws Error if the database query fails */ -export async function getAmountOfMembersOfClass(props: { - className: string; - schoolName: string; -}) { +export async function getAmountOfMembersOfClass(props: ClassIdentifier) { const selectClassMembersQuery = e.select(e.User, (u) => { const classNameMatches = e.op( u[" countClassMembersQuery.run(client)); - if (result.status === "error") { + if (result.isError) { throw new Error("Failed to get amount of class members"); } return result.data; } + +/** + * Find out if a class exists + * It doesn't modify the database but reads from it + * @throws Error if the query fails + * @readonly + */ +export async function doesClassExist(props: ClassIdentifier) { + const query = e.select(e.Class, (c) => { + const nameMatches = e.op(c.name, '=', props.className); + const schoolMatches = e.op(c.school.name, '=', props.schoolName); + return { + filter_single: e.op(nameMatches, 'and', schoolMatches) + }; + }); + const result = await promiseResult(() => query.run(client)) + + if (result.isError) { + throw new Error("Failed to get class"); + } + + return !!result.data +} diff --git a/tests/date.test.ts b/tests/date.test.ts new file mode 100644 index 0000000..f9adfee --- /dev/null +++ b/tests/date.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "bun:test"; +import { currentCustomDate, currentDay } from "utils/dates/current"; + +describe("current date", () => { + it("current day", () => { + const day = currentDay(); + expect(day).toBeGreaterThanOrEqual(1); + expect(day).toBeLessThanOrEqual(31); + }); + it("current custom date", () => { + const date = currentCustomDate(); + + expect(date.day).toBeGreaterThanOrEqual(1); + expect(date.day).toBeLessThanOrEqual(31); + expect(date.month).toBeGreaterThanOrEqual(1); + expect(date.month).toBeLessThanOrEqual(12); + expect(date.year).toBeGreaterThanOrEqual(1970); + }); +});