diff --git a/src/constants/documentation.ts b/src/constants/documentation.ts index 9b61f6d..6d7e5a5 100644 --- a/src/constants/documentation.ts +++ b/src/constants/documentation.ts @@ -1,3 +1,4 @@ +import type { ElysiaSwaggerConfig } from "@elysiajs/swagger/dist/types"; import { VERSION } from "./general"; /** @@ -25,4 +26,4 @@ export const DOCUMENTATION_OPTIONS = { { name: "User", description: "User information endpoints" }, ], }, -}; +} satisfies ElysiaSwaggerConfig<"/swagger">; diff --git a/src/index.ts b/src/index.ts index e5be797..f072715 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import { Elysia } from "elysia"; import { accessTokenRouter } from "routes/auth/accessToken"; import { refreshTokenRouter } from "routes/auth/refreshToken"; import { registerRouter } from "routes/auth/register"; +import { classRouter } from "routes/classes"; import { schoolRouter } from "routes/school"; import { userInfoRouter } from "routes/user/info"; import { edgedb } from "../dbschema/edgeql-js/imports"; @@ -14,6 +15,7 @@ export const client = edgedb.createClient(); const app = new Elysia() .use(swagger(DOCUMENTATION_OPTIONS)) .use(schoolRouter) + .use(classRouter) .get( "/", () => ({ diff --git a/src/routes/classes/create.ts b/src/routes/classes/create.ts new file mode 100644 index 0000000..04266fe --- /dev/null +++ b/src/routes/classes/create.ts @@ -0,0 +1,89 @@ +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"; +import { z } from "zod"; + +const classesSchema = z.object({ + classes: z + .array( + z.object({ + id: z.string(), + }), + ) + .max(1) + .min(1), +}); + +export const createClass = new Elysia() + .use(auth) + .use(HttpStatusCode()) + .post( + "/", + async ({ body, auth, set, httpStatus }) => { + if (!auth.isAuthorized) { + set.status = httpStatus.HTTP_401_UNAUTHORIZED; + return UNAUTHORIZED; + } + + const classesQuery = e.select(e.School, (s) => ({ + filter_single: e.op(s.name, "=", body.school), + classes: (c) => ({ + filter_single: e.op(c.name, "=", body.name), + }), + })); + const result = await promiseResult(() => classesQuery.run(client)); + if (result.status === "error") { + set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR; + return DATABASE_READ_FAILED; + } + const classNameIsInUse = classesSchema.safeParse(result.data).success; + if (classNameIsInUse) { + set.status = httpStatus.HTTP_400_BAD_REQUEST; + return responseBuilder("error", { + error: `A class with the name ${body.name} already exists in the school ${body.school}`, + }); + } + + const createClassQuery = e.update(e.School, (s) => ({ + filter_single: e.op(s.name, "=", body.school), + set: { + classes: { "+=": e.insert(e.Class, { name: body.name }) }, + }, + })); + const createResult = await promiseResult(() => + createClassQuery.run(client), + ); + if (createResult.status === "error") { + set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR; + return DATABASE_WRITE_FAILED; + } + + set.status = httpStatus.HTTP_201_CREATED; + return responseBuilder("success", { + message: `Successfully created class ${body.name}`, + data: null, + }); + }, + { + body: t.Object({ + name: t.String({ + minLength: 1, + description: "The name of the class", + examples: ["10a", "Dolphins", "Mathe LK", "Latein GK"], + }), + school: t.String({ + minLength: 1, + description: "The name of the school the class belongs to", + }), + }), + }, + ); diff --git a/src/routes/classes/get.ts b/src/routes/classes/get.ts new file mode 100644 index 0000000..ccc59cc --- /dev/null +++ b/src/routes/classes/get.ts @@ -0,0 +1,70 @@ +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 { replaceDateWithTimestamp } from "utils/objects/transform"; +import { responseBuilder } from "utils/response"; + +export const getClass = new Elysia().use(HttpStatusCode()).get( + "/", + async ({ query, set, httpStatus }) => { + const classesQuery = e.select(e.School, (s) => ({ + filter_single: e.op(s.name, "=", query.school), + classes: (c) => ({ + limit: query.limit, + offset: query.offset, + filter: query.query + ? e.op(c.name, "like", `%${query.query}%`) + : undefined, + + name: true, + created: true, + }), + })); + const result = await promiseResult(() => classesQuery.run(client)); + + if (result.status === "error") { + set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR; + return DATABASE_READ_FAILED; + } + const { data } = result; + if (!data) { + set.status = httpStatus.HTTP_404_NOT_FOUND; + return responseBuilder("error", { + error: `The school ${query.school} doesn't exist`, + }); + } + + return responseBuilder("success", { + message: "Successfully retrieved classes", + data: data.classes.map(replaceDateWithTimestamp), + }); + }, + { + query: t.Object({ + school: t.String({ + minLength: 1, + description: "The name of the school to get the classes from", + }), + limit: t.Numeric({ + minimum: 1, + maximum: 100, + default: 20, + description: "The maximum number of classes to retrieve", + }), + offset: t.Numeric({ + minimum: 0, + default: 0, + description: "The number of classes to skip", + }), + query: t.Optional( + t.String({ + minLength: 1, + description: "A query to search for classes", + }), + ), + }), + }, +); diff --git a/src/routes/classes/index.ts b/src/routes/classes/index.ts new file mode 100644 index 0000000..f494a91 --- /dev/null +++ b/src/routes/classes/index.ts @@ -0,0 +1,7 @@ +import Elysia from "elysia"; +import { createClass } from "./create"; +import { getClass } from "./get"; + +export const classRouter = new Elysia({ prefix: "/classes" }) + .use(getClass) + .use(createClass);