diff --git a/src/index.ts b/src/index.ts index 84c24a0..ef067c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import { calendarRouter } from "routes/calendar"; import { classRouter } from "routes/classes"; import { moderationRouter } from "routes/moderation"; import { schoolRouter } from "routes/school"; +import { tagRouter } from "routes/tags"; import { deleteUser } from "routes/user/delete"; import { userInfoRouter } from "routes/user/info"; import { changeUserDetailsRouter } from "routes/user/settings"; @@ -24,6 +25,7 @@ const app = new Elysia() .use(moderationRouter) .use(assignmentsRouter) .use(calendarRouter) + .use(tagRouter) .get( "/", () => ({ diff --git a/src/routes/tags/create.ts b/src/routes/tags/create.ts new file mode 100644 index 0000000..6414b14 --- /dev/null +++ b/src/routes/tags/create.ts @@ -0,0 +1,94 @@ +import e from "@edgedb"; +import { + DATABASE_READ_FAILED, + DATABASE_WRITE_FAILED, + 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 { promiseResult } from "utils/errors"; +import { responseBuilder } from "utils/response"; + +export const createTag = new Elysia() + .use(auth) + .use(HttpStatusCode()) + .post( + "/", + async ({ set, body, auth, httpStatus }) => { + if (!auth.isAuthorized) { + set.status = httpStatus.HTTP_401_UNAUTHORIZED; + return UNAUTHORIZED; + } + + const classQuery = e.select(e.Class, (c) => ({ + filter_single: e.op( + e.op(auth.username, "in", c.students.username), + "and", + e.op(c.name, "=", body.class), + ), + })); + + const tagsQuery = e.select(e.Tag, (t) => ({ + filter: e.op( + e.op(e.str_lower(t.tag), "=", body.tag.toLowerCase()), + "and", + e.op( + e.op(t.class.name, "=", body.class), + "and", + e.op(auth.username, "in", t.class.students.username), + ), + ), + })); + + const vaidationlResult = await promiseResult(async () => { + const [classes, tags] = await Promise.all([ + e.count(classQuery).run(client), + e.count(tagsQuery).run(client), + ]); + + return { classes, tags }; + }); + if (vaidationlResult.isError) { + set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR; + return DATABASE_READ_FAILED; + } + if (vaidationlResult.data.tags > 0) { + set.status = httpStatus.HTTP_400_BAD_REQUEST; + return responseBuilder("error", { error: "This tag already exists" }); + } + if (vaidationlResult.data.classes === 0) { + set.status = httpStatus.HTTP_404_NOT_FOUND; + return responseBuilder("error", { + error: "This class doesn't exist or you need to join it", + }); + } + + const query = e.insert(e.Tag, { + tag: body.tag, + class: classQuery, + color: body.color, + }); + + const result = await promiseResult(() => query.run(client)); + + if (result.isError) { + set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR; + return DATABASE_WRITE_FAILED; + } + + set.status = httpStatus.HTTP_201_CREATED; + return responseBuilder("success", { + message: "Created a new tag", + data: null, + }); + }, + { + body: t.Object({ + tag: t.String({ minLength: 1 }), + class: t.String({ minLength: 1 }), + color: t.Optional(t.RegExp(/^#[0-9a-f]{3}([0-9a-f]{3})?$/)), + }), + }, + ); diff --git a/src/routes/tags/index.ts b/src/routes/tags/index.ts new file mode 100644 index 0000000..cf7884a --- /dev/null +++ b/src/routes/tags/index.ts @@ -0,0 +1,7 @@ +import Elysia from "elysia"; +import { createTag } from "./create"; +import { listTags } from "./list"; + +export const tagRouter = new Elysia({ prefix: "/tags" }) + .use(createTag) + .use(listTags); diff --git a/src/routes/tags/list.ts b/src/routes/tags/list.ts new file mode 100644 index 0000000..dc87f53 --- /dev/null +++ b/src/routes/tags/list.ts @@ -0,0 +1,54 @@ +import e from "@edgedb"; +import { DATABASE_READ_FAILED } from "constants/responses"; +import Elysia, { t } from "elysia"; +import { HttpStatusCode } from "elysia-http-status-code"; +import { client } from "index"; +import { promiseResult } from "utils/errors"; +import { responseBuilder } from "utils/response"; +import { surround } from "utils/strings/surround"; + +export const listTags = new Elysia().use(HttpStatusCode()).get( + "/", + async ({ set, httpStatus, query }) => { + const dbQuery = e.select(e.Tag, (t) => { + const queryFilter = query.query + ? e.op(t.tag, "ilike", surround(query.query, "%")) + : e.bool(true); + + const classFilter = e.op( + e.op(t.class.name, "=", query.class), + "and", + e.op(t.class.school.name, "=", query.school), + ); + + return { + filter: e.op(queryFilter, "and", classFilter), + offset: query.offset, + limit: query.limit === -1 ? undefined : query.limit, + + tag: true, + color: true, + }; + }); + + const result = await promiseResult(() => dbQuery.run(client)); + if (result.isError) { + set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR; + return DATABASE_READ_FAILED; + } + + return responseBuilder("success", { + message: "Successfully retrieved tags", + data: result.data, + }); + }, + { + query: t.Object({ + class: t.String(), + school: t.String(), + query: t.Optional(t.String()), + limit: t.Numeric({ minimum: 0, default: 50 }), + offset: t.Numeric({ minimum: 0, default: 0 }), + }), + }, +);