diff --git a/README.md b/README.md index 299ecbf..5c34de1 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ Classical homework books have a lot of problems. These are some of them and how This is the second Version of the backend. Here is a brief roadmap until it will be integrated into the Frontend: -- [ ] Improvements to the authentication system -- [ ] Notes - [ ] Calendar +- [ ] Tags +- [ ] Notes - [ ] A SDK for TypeScript - [ ] Migrating the Frontend from the old to the new API - [ ] Migrating the data from MongoDB to EdgeDB diff --git a/dbschema/default.esdl b/dbschema/default.esdl index e7a0395..15ead66 100644 --- a/dbschema/default.esdl +++ b/dbschema/default.esdl @@ -116,6 +116,40 @@ module default { }; } + scalar type Priority extending enum; + + type Calendar { + required class: Class { + readonly := true; + }; + + + required title: str; + summary: str; + + required beginning: datetime; + # It may sound stupid, but in school we don't + # always know how long something will take, so it is optional + ending: datetime; + + location: str; + + + multi tags: Tag; + priority: Priority; + + multi updates: Change { + on target delete allow; + on source delete delete target; + }; + } + + type Tag { + required tag: str; + color: str; + required class: Class; + } + type Change { required user: User { on target delete delete source; @@ -125,5 +159,6 @@ module default { }; assignments := .; + CREATE TYPE default::Calendar { + CREATE REQUIRED LINK class: default::Class { + SET readonly := true; + }; + CREATE MULTI LINK tags: default::Tag; + CREATE MULTI LINK updates: default::Change { + ON SOURCE DELETE DELETE TARGET; + ON TARGET DELETE ALLOW; + }; + CREATE REQUIRED PROPERTY beginning: std::datetime; + CREATE PROPERTY ending: std::datetime; + CREATE PROPERTY location: std::str; + CREATE PROPERTY priority: default::Priority; + CREATE PROPERTY summary: std::str; + CREATE REQUIRED PROPERTY title: std::str; + }; + ALTER TYPE default::Change { + CREATE LINK calendar := (. ({ diff --git a/src/routes/calendar/create.ts b/src/routes/calendar/create.ts new file mode 100644 index 0000000..faba385 --- /dev/null +++ b/src/routes/calendar/create.ts @@ -0,0 +1,110 @@ +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 { isIncreasing } from "utils/arrays/increasing"; +import { promiseResult } from "utils/errors"; +import { classBySchoolAndName } from "utils/queries/class"; +import { responseBuilder } from "utils/response"; +import { savePredicate } from "utils/undefined"; + +export const createCalendar = new Elysia() + .use(HttpStatusCode()) + .use(auth) + .post( + "/", + async ({ body, auth, set, httpStatus }) => { + if (!auth.isAuthorized) { + set.status = httpStatus.HTTP_401_UNAUTHORIZED; + return UNAUTHORIZED; + } + + if (!isIncreasing([body.beginning, body.ending ?? Infinity])) { + set.status = httpStatus.HTTP_422_UNPROCESSABLE_ENTITY; + return responseBuilder("error", { + error: "Ending must be later then beginning" + }) + } + + const isUserInClassQuery = e.count( + e.select(e.Class, (c) => ({ + filter_single: e.op( + e.op( + e.op(c.name, "=", body.class), + "and", + e.op(c.school.name, "=", body.school), + ), + "and", + e.op(auth.username, "in", c.students.username), + ), + })), + ); + + const updateQuery = e.insert(e.Calendar, { + title: body.title, + summary: body.summary, + class: classBySchoolAndName({ + className: body.class, + schoolName: body.school, + }), + beginning: new Date(body.beginning), + ending: savePredicate(body.ending, (ending) => new Date(ending)), + updates: e.insert(e.Change, { + user: e.select(e.User, (u) => ({ + filter_single: e.op(u.username, "=", auth.username), + })), + }), + location: body.location, + priority: body.priority, + }); + + const result = await promiseResult(() => { + return client.transaction(async (tx) => { + const isInClass = await isUserInClassQuery + .run(tx) + .then((c) => c === 1); + if (!isInClass) return "NOT_IN_CLASS"; + + return updateQuery.run(tx); + }); + }); + + if (result.isError) { + set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR; + return DATABASE_WRITE_FAILED; + } + + if (result.data === "NOT_IN_CLASS") { + set.status = httpStatus.HTTP_404_NOT_FOUND; + return responseBuilder("error", { + error: + "This class doesn't exist or you don't have rights to create data for it", + }); + } + + set.status = httpStatus.HTTP_201_CREATED; + return responseBuilder("success", { + message: "Successfully created calendar event", + data: result.data, + }); + }, + { + body: t.Object({ + title: t.String({ minLength: 1 }), + school: t.String({ minLength: 1 }), + class: t.String({ minLength: 1 }), + beginning: t.Number({ minimum: 0 }), + ending: t.Optional(t.Number({ minimum: 1 })), + summary: t.Optional(t.String({ minLength: 1 })), + location: t.Optional(t.String({ minLength: 1 })), + priority: t.Optional(t.Union([ + t.Literal("Critical"), + t.Literal("High"), + t.Literal("Medium"), + t.Literal("Low"), + ])) + }), + }, + ); diff --git a/src/routes/calendar/index.ts b/src/routes/calendar/index.ts new file mode 100644 index 0000000..72c73e0 --- /dev/null +++ b/src/routes/calendar/index.ts @@ -0,0 +1,6 @@ +import Elysia from "elysia"; +import { createCalendar } from "./create"; + +export const calendarRouter = new Elysia({ prefix: "/calendar" }).use( + createCalendar, +); diff --git a/src/utils/arrays/increasing.ts b/src/utils/arrays/increasing.ts new file mode 100644 index 0000000..2e2ead3 --- /dev/null +++ b/src/utils/arrays/increasing.ts @@ -0,0 +1,8 @@ +export const isIncreasing = (arr: number[]): boolean => { + if (arr.length <= 1) return true + + const [first, second] = arr.slice(0, 2) + if (second < first) return false + + return isIncreasing(arr.slice(1)) +} diff --git a/src/utils/queries/README.md b/src/utils/queries/README.md new file mode 100644 index 0000000..6e2b382 --- /dev/null +++ b/src/utils/queries/README.md @@ -0,0 +1,6 @@ +# EdgeDB Queries + +The functions in this directory should only make it simpler to create queries. +These functions are not supposed to execute those queries. This makes it way +clearer what does what and makes it possible to use these queries in +transactions. diff --git a/src/utils/queries/class.ts b/src/utils/queries/class.ts new file mode 100644 index 0000000..192c87f --- /dev/null +++ b/src/utils/queries/class.ts @@ -0,0 +1,19 @@ +import e from "@edgedb"; + +interface ClassBySchoolAndNameProps { + schoolName: string; + className: string; +} + +export const classBySchoolAndName = ({ + schoolName, + className, +}: ClassBySchoolAndNameProps) => { + return e.select(e.Class, (c) => ({ + filter_single: e.op( + e.op(c.name, "=", className), + "and", + e.op(c.school.name, "=", schoolName), + ), + })); +}; diff --git a/tests/array.test.ts b/tests/array.test.ts index b808d5c..7ae19aa 100644 --- a/tests/array.test.ts +++ b/tests/array.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "bun:test"; import { removeDuplicates } from "utils/arrays/duplicates"; import { areSameValue } from "utils/arrays/general"; +import { isIncreasing } from "utils/arrays/increasing"; import { merge } from "utils/arrays/merge"; describe("merge", () => { @@ -70,3 +71,22 @@ describe("same value", () => { } }); }); + +describe("is increasing", () => { + it("works for short arrays", () => { + expect(isIncreasing([])).toBeTrue() + expect(isIncreasing([1])).toBeTrue() + expect(isIncreasing([-1])).toBeTrue() + }) + + it("works for increasing long arrays", () => { + expect(isIncreasing([1,2,3,4,60,70])).toBeTrue() + expect(isIncreasing([-Infinity, -2.5,300, Infinity])).toBeTrue() + }) + + it("works for non increasing long arrays", () => { + expect(isIncreasing([-Infinity, -200, 0, 1, 2, 4, 3, 5])).toBeFalse() + expect(isIncreasing([1,0,2,3,4])).toBeFalse() + expect(isIncreasing([0,1,2,4,3])).toBeFalse() + }) +})