diff --git a/src/routes/assignments/list.ts b/src/routes/assignments/list.ts index da8be01..2f62e87 100644 --- a/src/routes/assignments/list.ts +++ b/src/routes/assignments/list.ts @@ -125,6 +125,10 @@ export const listAssignments = new Elysia().use(HttpStatusCode()).get( updates: () => ({ user: () => ({ username: true, displayname: true }), time: true, + updates: () => ({ + user: () => ({ username: true, displayname: true }), + time: true, + }), }), id: true, }; diff --git a/src/routes/calendar/list.ts b/src/routes/calendar/list.ts index 66293dc..7609359 100644 --- a/src/routes/calendar/list.ts +++ b/src/routes/calendar/list.ts @@ -11,6 +11,7 @@ import { normalDateToCustom, stringToNormal, } from "utils/dates/customAndNormal"; +import { strToDir } from "utils/db/direction"; import { promiseResult } from "utils/errors"; import { replaceDateDeep } from "utils/objects/transform"; import { responseBuilder } from "utils/response"; @@ -32,8 +33,6 @@ export const listCalendar = new Elysia().use(HttpStatusCode()).get( } const classNames = removeDuplicates(classesResult.data).sort(); - const isDescending = query.orderDirection === "desc"; - const latestStart = savePredicate( query.filter?.start?.latest, stringToNormal, @@ -85,7 +84,7 @@ export const listCalendar = new Elysia().use(HttpStatusCode()).get( ), order_by: { expression: orderExpression, - direction: isDescending ? e.DESC : e.ASC, + direction: strToDir(query.orderDirection), empty: e.EMPTY_LAST, }, limit: internalLimit, diff --git a/src/routes/notes/index.ts b/src/routes/notes/index.ts index 41ed7e7..4a97718 100644 --- a/src/routes/notes/index.ts +++ b/src/routes/notes/index.ts @@ -1,4 +1,7 @@ import Elysia from "elysia"; import { createNote } from "./create"; +import { listNotes } from "./list"; -export const noteRouter = new Elysia({ prefix: "/notes" }).use(createNote); +export const noteRouter = new Elysia({ prefix: "/notes" }) + .use(createNote) + .use(listNotes); diff --git a/src/routes/notes/list.ts b/src/routes/notes/list.ts new file mode 100644 index 0000000..f8b3944 --- /dev/null +++ b/src/routes/notes/list.ts @@ -0,0 +1,136 @@ +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 { auth } from "plugins/auth"; +import { stringArraySchema } from "schemas/stringArray"; +import { removeDuplicates } from "utils/arrays/duplicates"; +import { filterTruthy } from "utils/arrays/filter"; +import { normalDateToCustom } from "utils/dates/customAndNormal"; +import { strToDir } from "utils/db/direction"; +import { promiseResult } from "utils/errors"; +import { replaceDateDeep } from "utils/objects/transform"; +import { responseBuilder } from "utils/response"; +import { split } from "utils/strings/split"; +import { surround } from "utils/strings/surround"; + +export const listNotes = new Elysia() + .use(HttpStatusCode()) + .use(auth) + .get( + "/", + async ({ set, httpStatus, query, auth }) => { + const classesResult = stringArraySchema.safeParse( + filterTruthy(split(query.classes)), + ); + if (!classesResult.success) { + set.status = httpStatus.HTTP_400_BAD_REQUEST; + return responseBuilder("error", { + error: "Classes must be an array of strings", + }); + } + const classNames = removeDuplicates(classesResult.data).sort(); + + const qFilter = query.filter?.query; + + const dbQuery = (limit: number, offset: number) => + e.select(e.Note, (n) => { + const internalLimit = limit === -1 ? undefined : limit; + + const classMatches = e.op( + e.op(n.class.name, "=", e.set(...classNames)), + "and", + e.op(n.class.school.name, "=", query.school), + ); + + const queryFilter = qFilter + ? e.op( + e.op(n.title, "ilike", surround(qFilter, "%")), + "or", + e.op(n.summary, "ilike", surround(qFilter, "%")), + ) + : e.bool(true); + + const orderExpression = { + title: n.title, + summary: n.summary, + "last-update": e.max(n.updates.time), + "first-update": e.min(n.updates.time), + }[query.orderKey]; + + return { + filter: e.op(classMatches, "and", queryFilter), + order_by: { + expression: orderExpression, + direction: strToDir(query.orderDirection), + empty: e.EMPTY_LAST, + }, + limit: internalLimit, + offset, + + title: true, + summary: true, + tags: () => ({ + tag: true, + color: true, + }), + editScope: auth.isAuthorized, + priority: true, + updates: () => ({ + user: () => ({ username: true, displayname: true }), + time: true, + }), + id: true, + }; + }); + const result = await promiseResult(() => + dbQuery(query.limit, query.offset).run(client), + ); + + if (result.isError) { + set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR; + return DATABASE_READ_FAILED; + } + + const formatted = result.data.map((i) => + replaceDateDeep(i, normalDateToCustom), + ); + + return responseBuilder("success", { + message: "Successfully retrieved data", + data: { + notes: formatted, + }, + }); + }, + { + query: t.Object({ + school: t.String({ minLength: 1 }), + classes: t.String({ minLength: 1 }), + + limit: t.Numeric({ minimum: -1, default: 15 }), + offset: t.Numeric({ minimum: 0, default: 0 }), + + orderDirection: t.Union([t.Literal("asc"), t.Literal("desc")], { + default: "desc", + }), + orderKey: t.Union( + [ + t.Literal("title"), + t.Literal("summary"), + t.Literal("last-update"), + t.Literal("first-update"), + ], + { + default: "last-update", + }, + ), + filter: t.Optional( + t.ObjectString({ + query: t.Optional(t.String()), + }), + ), + }), + }, + ); diff --git a/src/utils/db/direction.ts b/src/utils/db/direction.ts new file mode 100644 index 0000000..7006bc8 --- /dev/null +++ b/src/utils/db/direction.ts @@ -0,0 +1,4 @@ +import e from "@edgedb"; + +export const strToDir = (dir: "asc" | "desc") => + dir === "asc" ? e.ASC : e.DESC;