Skip to content

Commit

Permalink
Add an endpoint to create new assignments
Browse files Browse the repository at this point in the history
  • Loading branch information
Dlurak committed Mar 31, 2024
1 parent 1409e4d commit 4129189
Show file tree
Hide file tree
Showing 12 changed files with 296 additions and 5 deletions.
23 changes: 23 additions & 0 deletions dbschema/default.esdl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module default {
};

classes := .<students[is Class];
assignments := .<updatedBy[is Assignment];
}

type RefreshToken {
Expand Down Expand Up @@ -62,6 +63,9 @@ module default {
};

single school := .<classes[is School];

assignments := .<class[is Assignment];

required created: datetime {
default := datetime_current();
readonly := true;
Expand Down Expand Up @@ -89,4 +93,23 @@ module default {
reviewedAt: datetime;
reviewedBy: User;
}

type Assignment {
required subject: str;
required description: str;

required dueDate: datetime;
required fromDate: datetime;

required multi updates: datetime {
default := datetime_current();
};
required multi updatedBy: User;

multi completedBy: User;

required class: Class {
readonly := true;
};
}
}
24 changes: 24 additions & 0 deletions dbschema/migrations/00010.edgeql
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
CREATE MIGRATION m1h2a4cqyvh3sfyprjlvecaakpxyoaz53e3egs4qdlf2zeapdzfvqa
ONTO m1g3bsb7rj3ypvusgnkekg6g7zetyzd7xvl564gqjkdb4zwjfphyyq
{
CREATE TYPE default::Assignment {
CREATE REQUIRED LINK class: default::Class {
SET readonly := true;
};
CREATE REQUIRED MULTI LINK completedBy: default::User;
CREATE REQUIRED MULTI LINK updatedBy: default::User;
CREATE REQUIRED PROPERTY description: std::str;
CREATE REQUIRED PROPERTY dueDate: std::datetime;
CREATE REQUIRED PROPERTY fromDate: std::datetime;
CREATE REQUIRED PROPERTY subject: std::str;
CREATE REQUIRED MULTI PROPERTY updates: std::datetime {
SET default := (std::datetime_current());
};
};
ALTER TYPE default::Class {
CREATE LINK assignments := (.<class[IS default::Assignment]);
};
ALTER TYPE default::User {
CREATE LINK assignments := (.<updatedBy[IS default::Assignment]);
};
};
9 changes: 9 additions & 0 deletions dbschema/migrations/00011.edgeql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE MIGRATION m1whcftw3ixmaofcafcskg2ttd5f4kpecobinvtejd66vge4p5whxq
ONTO m1h2a4cqyvh3sfyprjlvecaakpxyoaz53e3egs4qdlf2zeapdzfvqa
{
ALTER TYPE default::Assignment {
ALTER LINK completedBy {
RESET OPTIONALITY;
};
};
};
8 changes: 8 additions & 0 deletions src/constants/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,11 @@ export const DATABASE_WRITE_FAILED = responseBuilder("error", {
export const DATABASE_READ_FAILED = responseBuilder("error", {
error: "An error occurred while reading from the database",
});

/**
* A response indicating that the user does not have permission to access the resource
* Use with `403 Forbidden`
*/
export const FORBIDDEN = responseBuilder("error", {
error: "Forbidden",
});
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { swagger } from "@elysiajs/swagger";
import { DOCUMENTATION_OPTIONS } from "constants/documentation";
import { VERSION } from "constants/general";
import { Elysia } from "elysia";
import { assignmentsRouter } from "routes/assignments";
import { accessTokenRouter } from "routes/auth/accessToken";
import { refreshTokenRouter } from "routes/auth/refreshToken";
import { registerRouter } from "routes/auth/register";
Expand All @@ -18,6 +19,7 @@ const app = new Elysia()
.use(schoolRouter)
.use(classRouter)
.use(moderationRouter)
.use(assignmentsRouter)
.get(
"/",
() => ({
Expand Down
124 changes: 124 additions & 0 deletions src/routes/assignments/create.ts
Original file line number Diff line number Diff line change
@@ -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 }),
}),
}),
},
);
6 changes: 6 additions & 0 deletions src/routes/assignments/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Elysia from "elysia";
import { createAssignment } from "./create";

export const assignmentsRouter = new Elysia({ prefix: "/assignments" }).use(
createAssignment,
);
5 changes: 5 additions & 0 deletions src/types/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type CustomDate = {
day: number;
month: number;
year: number;
};
27 changes: 27 additions & 0 deletions src/utils/dates/current.ts
Original file line number Diff line number Diff line change
@@ -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(),
});
19 changes: 19 additions & 0 deletions src/utils/dates/customAndNormal.ts
Original file line number Diff line number Diff line change
@@ -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 };
};
35 changes: 30 additions & 5 deletions src/utils/db/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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["<students[is Class]"].name,
Expand All @@ -31,9 +33,32 @@ export async function getAmountOfMembersOfClass(props: {
const countClassMembersQuery = e.count(selectClassMembersQuery);
const result = await promiseResult(() => 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
}
19 changes: 19 additions & 0 deletions tests/date.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit 4129189

Please sign in to comment.