diff --git a/dbschema/default.esdl b/dbschema/default.esdl index e4b8802..22d152f 100644 --- a/dbschema/default.esdl +++ b/dbschema/default.esdl @@ -146,6 +146,31 @@ module default { }; } + scalar type EditScope extending enum; + + type Note { + required class: Class { + readonly := true; + }; + + required title: str; + summary: str; + + multi tags: Tag { + on target delete allow; + }; + priority: Priority; + + required editScope: EditScope { + default := EditScope.Self; + }; + + multi updates: Change { + on target delete allow; + on source delete delete target; + }; + } + type Tag { required tag: str; color: str; diff --git a/dbschema/migrations/00019.edgeql b/dbschema/migrations/00019.edgeql new file mode 100644 index 0000000..e3eaffe --- /dev/null +++ b/dbschema/migrations/00019.edgeql @@ -0,0 +1,28 @@ +CREATE MIGRATION m1mzn3ehkgkjrnujdwfxjhe5optsk3cmyqkdagwev6vq2p5a4xxbsa + ONTO m1xsbfzwxprtxttepfmhlwzsayxv3keyigzrfa2o2zk3g4zttajubq +{ + ALTER TYPE default::Calendar { + ALTER LINK tags { + ON TARGET DELETE ALLOW; + }; + }; + CREATE SCALAR TYPE default::EditScope EXTENDING enum; + CREATE TYPE default::Note { + CREATE REQUIRED LINK class: default::Class { + SET readonly := true; + }; + CREATE MULTI LINK tags: default::Tag { + ON TARGET DELETE ALLOW; + }; + CREATE MULTI LINK updates: default::Change { + ON SOURCE DELETE DELETE TARGET; + ON TARGET DELETE ALLOW; + }; + CREATE PROPERTY editScope: default::EditScope { + SET default := (default::EditScope.Self); + }; + CREATE PROPERTY priority: default::Priority; + CREATE PROPERTY summary: std::str; + CREATE REQUIRED PROPERTY title: std::str; + }; +}; diff --git a/src/index.ts b/src/index.ts index ef067c8..eb25c9a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import { registerRouter } from "routes/auth/register"; import { calendarRouter } from "routes/calendar"; import { classRouter } from "routes/classes"; import { moderationRouter } from "routes/moderation"; +import { noteRouter } from "routes/notes"; import { schoolRouter } from "routes/school"; import { tagRouter } from "routes/tags"; import { deleteUser } from "routes/user/delete"; @@ -26,6 +27,7 @@ const app = new Elysia() .use(assignmentsRouter) .use(calendarRouter) .use(tagRouter) + .use(noteRouter) .get( "/", () => ({ diff --git a/src/routes/notes/create.ts b/src/routes/notes/create.ts new file mode 100644 index 0000000..eade8c3 --- /dev/null +++ b/src/routes/notes/create.ts @@ -0,0 +1,83 @@ +import e from "@edgedb"; +import { 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 { fallback } from "utils/arrays/fallback"; +import { promiseResult } from "utils/errors"; +import { usersClassBySchoolAndName } from "utils/queries/class"; +import { userByUsername } from "utils/queries/user"; +import { responseBuilder } from "utils/response"; + +export const createNote = new Elysia() + .use(auth) + .use(HttpStatusCode()) + .post( + "/", + async ({ set, httpStatus, auth, body }) => { + if (!auth.isAuthorized) { + set.status = httpStatus.HTTP_401_UNAUTHORIZED; + return UNAUTHORIZED; + } + + const query = e.insert(e.Note, { + title: body.title, + class: usersClassBySchoolAndName({ + schoolName: body.school, + className: body.class, + username: auth.username, + }), + + summary: body.summary, + priority: body.priority, + tags: e.select(e.Tag, (t) => ({ + filter: e.op( + e.op( + e.op(t.class.name, "=", body.class), + "and", + e.op(t.class.school.name, "=", body.school), + ), + "and", + e.op(t.tag, "in", e.set(...fallback(body.tags || [], [""]))), + ), + })), + editScope: body.editScope, + + updates: e.insert(e.Change, { user: userByUsername(auth.username) }), + }); + 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: "Successfully created note", + data: null, + }); + }, + { + body: t.Object({ + class: t.String({ minLength: 1 }), + school: t.String({ minLength: 1 }), + title: t.String({ minLength: 1 }), + summary: t.Optional(t.String({ minLength: 1 })), + tags: t.Optional(t.Array(t.String({ minLength: 1 }))), + priority: t.Optional( + t.Union([ + t.Literal("Critical"), + t.Literal("High"), + t.Literal("Medium"), + t.Literal("Low"), + t.Literal("Minimal"), + ]), + ), + editScope: t.Optional( + t.Union([t.Literal("Self"), t.Literal("Class"), t.Literal("School")]), + ), + }), + }, + ); diff --git a/src/routes/notes/index.ts b/src/routes/notes/index.ts new file mode 100644 index 0000000..41ed7e7 --- /dev/null +++ b/src/routes/notes/index.ts @@ -0,0 +1,4 @@ +import Elysia from "elysia"; +import { createNote } from "./create"; + +export const noteRouter = new Elysia({ prefix: "/notes" }).use(createNote); diff --git a/src/utils/queries/class.ts b/src/utils/queries/class.ts index 192c87f..57e9882 100644 --- a/src/utils/queries/class.ts +++ b/src/utils/queries/class.ts @@ -5,6 +5,10 @@ interface ClassBySchoolAndNameProps { className: string; } +type UsersClassBySchoolAndNameProps = ClassBySchoolAndNameProps & { + username: string; +}; + export const classBySchoolAndName = ({ schoolName, className, @@ -17,3 +21,19 @@ export const classBySchoolAndName = ({ ), })); }; + +export const usersClassBySchoolAndName = ( + props: UsersClassBySchoolAndNameProps, +) => { + return e.select(e.Class, (c) => ({ + filter_single: e.op( + e.op( + e.op(c.name, "=", props.className), + "and", + e.op(c.school.name, "=", props.schoolName), + ), + "and", + e.op(props.username, "in", c.students.username), + ), + })); +}; diff --git a/src/utils/queries/user.ts b/src/utils/queries/user.ts new file mode 100644 index 0000000..6c52b53 --- /dev/null +++ b/src/utils/queries/user.ts @@ -0,0 +1,6 @@ +import e from "@edgedb"; + +export const userByUsername = (username: string) => + e.select(e.User, (u) => ({ + filter_single: e.op(u.username, "=", username), + }));