-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add an endpoint to retrieve assignments
- Loading branch information
Showing
10 changed files
with
290 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
import Elysia from "elysia"; | ||
import { createAssignment } from "./create"; | ||
import { listAssignments } from "./list"; | ||
|
||
export const assignmentsRouter = new Elysia({ prefix: "/assignments" }).use( | ||
createAssignment, | ||
); | ||
export const assignmentsRouter = new Elysia({ prefix: "/assignments" }) | ||
.use(listAssignments) | ||
.use(createAssignment); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
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 { removeDuplicates } from "utils/arrays/duplicates"; | ||
import { filterTruthy } from "utils/arrays/filter"; | ||
import { areSameValue } from "utils/arrays/general"; | ||
import { merge } from "utils/arrays/merge"; | ||
import { normalDateToCustom } from "utils/dates/customAndNormal"; | ||
import { multipleClasses } from "utils/db/classes"; | ||
import { promiseResult } from "utils/errors"; | ||
import { responseBuilder } from "utils/response"; | ||
import { split } from "utils/strings/split"; | ||
import { z } from "zod"; | ||
|
||
const classesSchema = z.array(z.string().min(1)).nonempty(); | ||
|
||
export const listAssignments = new Elysia().use(HttpStatusCode()).get( | ||
"/", | ||
async ({ query, set, httpStatus }) => { | ||
const classesResult = classesSchema.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 assignmentsQuery = (limit: number, offset: number) => | ||
e.select(e.Assignment, (a) => { | ||
const classMatches = e.op(a.class.name, "in", e.set(...classNames)); | ||
const schoolMatches = e.op(a.class.school.name, "=", query.school); | ||
|
||
// -1 disables the limit | ||
const internalLimit = limit === -1 ? undefined : limit; | ||
|
||
return { | ||
filter: e.op(classMatches, "and", schoolMatches), | ||
limit: internalLimit, | ||
offset, | ||
|
||
subject: true, | ||
description: true, | ||
dueDate: true, | ||
fromDate: true, | ||
updates: true, | ||
updatedBy: () => ({ username: true }), | ||
}; | ||
}); | ||
|
||
const result = await promiseResult(() => { | ||
return client.transaction(async (tx) => { | ||
const assignments = await assignmentsQuery( | ||
query.limit, | ||
query.offset, | ||
).run(tx); | ||
const count = await e.count(assignmentsQuery(-1, 0)).run(tx); | ||
const classes = await multipleClasses({ | ||
schoolName: query.school, | ||
classNames: classNames, | ||
}) | ||
.run(tx) | ||
.then((c) => (c ? c.map((cl) => cl.name).sort() : [])); | ||
|
||
return { assignments, count, classes }; | ||
}); | ||
}); | ||
|
||
if (result.isError) { | ||
set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR; | ||
return DATABASE_READ_FAILED; | ||
} | ||
|
||
if (!areSameValue(result.data.classes, classNames)) { | ||
set.status = httpStatus.HTTP_404_NOT_FOUND; | ||
return responseBuilder("error", { | ||
error: "Not all of the specified classes exist", | ||
}); | ||
} | ||
|
||
const formatted = result.data.assignments.map((assignment) => { | ||
const updates = merge( | ||
{ | ||
key: "user", | ||
array: assignment.updatedBy.map((u) => u.username), | ||
}, | ||
{ | ||
key: "timestamp", | ||
array: assignment.updates.map((d) => d.getTime()), | ||
}, | ||
); | ||
|
||
return { | ||
subject: assignment.subject, | ||
description: assignment.description, | ||
from: normalDateToCustom(assignment.fromDate), | ||
due: normalDateToCustom(assignment.dueDate), | ||
updates, | ||
}; | ||
}); | ||
|
||
return responseBuilder("success", { | ||
message: "Received data", | ||
data: { | ||
totalCount: result.data.count, | ||
assignments: formatted, | ||
}, | ||
}); | ||
}, | ||
{ | ||
query: t.Object({ | ||
school: t.String({ minLength: 1 }), | ||
classes: t.String({ minLength: 1 }), | ||
limit: t.Numeric({ minimum: -1, default: 50 }), | ||
offset: t.Numeric({ minimum: 0, default: 0 }), | ||
}), | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/** | ||
* Remove duplicates from an array | ||
* The original array will not be modified | ||
* The order will not be changed | ||
*/ | ||
export const removeDuplicates = <T>(arr: T[]) => { | ||
return arr.filter((val, i, self) => self.indexOf(val) === i); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/** | ||
* Filter out all falsy values from an array without modifying it | ||
*/ | ||
export const filterTruthy = <T>(array: T[]) => array.filter((i) => i); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
export const areSameValue = <T>(first: T[], second: T[]): boolean => { | ||
if (first.length !== second.length) return false; | ||
if (first.length === 0) return true; | ||
|
||
const firstArrFirst = first[0]; | ||
const secondArrFirst = first[0]; | ||
|
||
if (firstArrFirst !== secondArrFirst) return false; | ||
|
||
return areSameValue(first.slice(1), second.slice(1)); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
interface MergeInput<T, K extends string> { | ||
array: T[]; | ||
key: K; | ||
} | ||
|
||
type MergeResult<F, FK extends string, S, SK extends string> = { | ||
[K in FK]: F | undefined; | ||
} & { | ||
[K in SK]: S | undefined; | ||
}; | ||
|
||
/** | ||
* Merge two arrays into one | ||
*/ | ||
export const merge = <F, FK extends string, S, SK extends string>( | ||
first: MergeInput<F, FK>, | ||
second: MergeInput<S, SK>, | ||
) => { | ||
type Rec = MergeResult<F, FK, S, SK>; | ||
|
||
const mergedArray: Rec[] = []; | ||
const maxLength = Math.max(first.array.length, second.array.length); | ||
|
||
for (let i = 0; i < maxLength; i++) { | ||
const entries = [ | ||
[first.key, first.array[i]], | ||
[second.key, second.array[i]], | ||
] as const; | ||
|
||
mergedArray.push(Object.fromEntries(entries) as Rec); | ||
} | ||
|
||
return mergedArray; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/** | ||
* Split a string at commas but not escaped ones | ||
*/ | ||
export const split = (str: string) => | ||
str.split(/(?<!\\),/).map((s) => s.replace(/\\,/g, ",")); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { describe, expect, it } from "bun:test"; | ||
import { removeDuplicates } from "utils/arrays/duplicates"; | ||
import { areSameValue } from "utils/arrays/general"; | ||
import { merge } from "utils/arrays/merge"; | ||
|
||
describe("merge", () => { | ||
it("same length", () => { | ||
const result = merge( | ||
{ key: "a", array: [1, 2, 3] }, | ||
{ key: "b", array: ["x", "y", "z"] }, | ||
); | ||
|
||
expect(result).toEqual([ | ||
{ a: 1, b: "x" }, | ||
{ a: 2, b: "y" }, | ||
{ a: 3, b: "z" }, | ||
]); | ||
}); | ||
|
||
it("works when the first one is longer", () => { | ||
const result = merge( | ||
{ key: "a", array: [1, 2, 3] }, | ||
{ key: "b", array: ["a"] }, | ||
); | ||
|
||
expect(result).toEqual([ | ||
{ a: 1, b: "a" }, | ||
{ a: 2, b: undefined }, | ||
{ a: 3, b: undefined }, | ||
]); | ||
}); | ||
|
||
it("works when the second one is longer", () => { | ||
const result = merge( | ||
{ key: "b", array: ["a"] }, | ||
{ key: "a", array: [1, 2, 3] }, | ||
); | ||
|
||
expect(result).toEqual([ | ||
{ b: "a", a: 1 }, | ||
{ b: undefined, a: 2 }, | ||
{ b: undefined, a: 3 }, | ||
]); | ||
}); | ||
}); | ||
|
||
describe("duplicates", () => { | ||
it("does nothing when there'no duplicates", () => { | ||
expect(removeDuplicates([1, 2, 3, 4])).toEqual([1, 2, 3, 4]); | ||
expect(removeDuplicates(["a", "b", "c"])).toEqual(["a", "b", "c"]); | ||
}); | ||
|
||
it("removes duplicates", () => { | ||
expect(removeDuplicates([1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1])).toEqual([ | ||
1, 2, | ||
]); | ||
expect( | ||
removeDuplicates(["ts", "rust", "linux", "i3", "linux", "js", "rust"]), | ||
).toEqual(["ts", "rust", "linux", "i3", "js"]); | ||
}); | ||
}); | ||
|
||
describe("same value", () => { | ||
it("works for equal arrays", () => { | ||
const testCases = [[], ["a", "a"], ["a", "b"], [1, 2], [1, 1]]; | ||
|
||
for (const tCase of testCases) { | ||
// @ts-ignore | ||
expect(areSameValue(tCase, tCase)).toBeTrue(); | ||
} | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { describe, expect, it } from "bun:test"; | ||
import { split } from "utils/strings/split"; | ||
|
||
describe("split", () => { | ||
it("splits at ,", () => { | ||
const original = "value,value2,valu5,"; | ||
expect(split(original)).toEqual(["value", "value2", "valu5", ""]); | ||
expect(original).toBe("value,value2,valu5,"); | ||
}); | ||
|
||
it("ignores escaped commas", () => { | ||
const original = "this\\,is\\,;one,and\\,this\\,another"; | ||
expect(split(original)).toEqual(["this,is,;one", "and,this,another"]); | ||
}); | ||
}); |