Skip to content

Commit 7f8d479

Browse files
committed
Add an endpoint to retrieve assignments
1 parent c386b92 commit 7f8d479

File tree

10 files changed

+290
-3
lines changed

10 files changed

+290
-3
lines changed

src/routes/assignments/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Elysia from "elysia";
22
import { createAssignment } from "./create";
3+
import { listAssignments } from "./list";
34

4-
export const assignmentsRouter = new Elysia({ prefix: "/assignments" }).use(
5-
createAssignment,
6-
);
5+
export const assignmentsRouter = new Elysia({ prefix: "/assignments" })
6+
.use(listAssignments)
7+
.use(createAssignment);

src/routes/assignments/list.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import e from "@edgedb";
2+
import { DATABASE_READ_FAILED } from "constants/responses";
3+
import Elysia, { t } from "elysia";
4+
import { HttpStatusCode } from "elysia-http-status-code";
5+
import { client } from "index";
6+
import { removeDuplicates } from "utils/arrays/duplicates";
7+
import { filterTruthy } from "utils/arrays/filter";
8+
import { areSameValue } from "utils/arrays/general";
9+
import { merge } from "utils/arrays/merge";
10+
import { normalDateToCustom } from "utils/dates/customAndNormal";
11+
import { multipleClasses } from "utils/db/classes";
12+
import { promiseResult } from "utils/errors";
13+
import { responseBuilder } from "utils/response";
14+
import { split } from "utils/strings/split";
15+
import { z } from "zod";
16+
17+
const classesSchema = z.array(z.string().min(1)).nonempty();
18+
19+
export const listAssignments = new Elysia().use(HttpStatusCode()).get(
20+
"/",
21+
async ({ query, set, httpStatus }) => {
22+
const classesResult = classesSchema.safeParse(
23+
filterTruthy(split(query.classes)),
24+
);
25+
if (!classesResult.success) {
26+
set.status = httpStatus.HTTP_400_BAD_REQUEST;
27+
return responseBuilder("error", {
28+
error: "Classes must be an array of strings",
29+
});
30+
}
31+
const classNames = removeDuplicates(classesResult.data).sort();
32+
33+
const assignmentsQuery = (limit: number, offset: number) =>
34+
e.select(e.Assignment, (a) => {
35+
const classMatches = e.op(a.class.name, "in", e.set(...classNames));
36+
const schoolMatches = e.op(a.class.school.name, "=", query.school);
37+
38+
// -1 disables the limit
39+
const internalLimit = limit === -1 ? undefined : limit;
40+
41+
return {
42+
filter: e.op(classMatches, "and", schoolMatches),
43+
limit: internalLimit,
44+
offset,
45+
46+
subject: true,
47+
description: true,
48+
dueDate: true,
49+
fromDate: true,
50+
updates: true,
51+
updatedBy: () => ({ username: true }),
52+
};
53+
});
54+
55+
const result = await promiseResult(() => {
56+
return client.transaction(async (tx) => {
57+
const assignments = await assignmentsQuery(
58+
query.limit,
59+
query.offset,
60+
).run(tx);
61+
const count = await e.count(assignmentsQuery(-1, 0)).run(tx);
62+
const classes = await multipleClasses({
63+
schoolName: query.school,
64+
classNames: classNames,
65+
})
66+
.run(tx)
67+
.then((c) => (c ? c.map((cl) => cl.name).sort() : []));
68+
69+
return { assignments, count, classes };
70+
});
71+
});
72+
73+
if (result.isError) {
74+
set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR;
75+
return DATABASE_READ_FAILED;
76+
}
77+
78+
if (!areSameValue(result.data.classes, classNames)) {
79+
set.status = httpStatus.HTTP_404_NOT_FOUND;
80+
return responseBuilder("error", {
81+
error: "Not all of the specified classes exist",
82+
});
83+
}
84+
85+
const formatted = result.data.assignments.map((assignment) => {
86+
const updates = merge(
87+
{
88+
key: "user",
89+
array: assignment.updatedBy.map((u) => u.username),
90+
},
91+
{
92+
key: "timestamp",
93+
array: assignment.updates.map((d) => d.getTime()),
94+
},
95+
);
96+
97+
return {
98+
subject: assignment.subject,
99+
description: assignment.description,
100+
from: normalDateToCustom(assignment.fromDate),
101+
due: normalDateToCustom(assignment.dueDate),
102+
updates,
103+
};
104+
});
105+
106+
return responseBuilder("success", {
107+
message: "Received data",
108+
data: {
109+
totalCount: result.data.count,
110+
assignments: formatted,
111+
},
112+
});
113+
},
114+
{
115+
query: t.Object({
116+
school: t.String({ minLength: 1 }),
117+
classes: t.String({ minLength: 1 }),
118+
limit: t.Numeric({ minimum: -1, default: 50 }),
119+
offset: t.Numeric({ minimum: 0, default: 0 }),
120+
}),
121+
},
122+
);

src/utils/arrays/duplicates.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Remove duplicates from an array
3+
* The original array will not be modified
4+
* The order will not be changed
5+
*/
6+
export const removeDuplicates = <T>(arr: T[]) => {
7+
return arr.filter((val, i, self) => self.indexOf(val) === i);
8+
};

src/utils/arrays/filter.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* Filter out all falsy values from an array without modifying it
3+
*/
4+
export const filterTruthy = <T>(array: T[]) => array.filter((i) => i);

src/utils/arrays/general.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const areSameValue = <T>(first: T[], second: T[]): boolean => {
2+
if (first.length !== second.length) return false;
3+
if (first.length === 0) return true;
4+
5+
const firstArrFirst = first[0];
6+
const secondArrFirst = first[0];
7+
8+
if (firstArrFirst !== secondArrFirst) return false;
9+
10+
return areSameValue(first.slice(1), second.slice(1));
11+
};

src/utils/arrays/merge.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
interface MergeInput<T, K extends string> {
2+
array: T[];
3+
key: K;
4+
}
5+
6+
type MergeResult<F, FK extends string, S, SK extends string> = {
7+
[K in FK]: F | undefined;
8+
} & {
9+
[K in SK]: S | undefined;
10+
};
11+
12+
/**
13+
* Merge two arrays into one
14+
*/
15+
export const merge = <F, FK extends string, S, SK extends string>(
16+
first: MergeInput<F, FK>,
17+
second: MergeInput<S, SK>,
18+
) => {
19+
type Rec = MergeResult<F, FK, S, SK>;
20+
21+
const mergedArray: Rec[] = [];
22+
const maxLength = Math.max(first.array.length, second.array.length);
23+
24+
for (let i = 0; i < maxLength; i++) {
25+
const entries = [
26+
[first.key, first.array[i]],
27+
[second.key, second.array[i]],
28+
] as const;
29+
30+
mergedArray.push(Object.fromEntries(entries) as Rec);
31+
}
32+
33+
return mergedArray;
34+
};

src/utils/db/classes.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,18 @@ export async function doesClassExist(props: ClassIdentifier) {
6262

6363
return !!result.data;
6464
}
65+
66+
export const multipleClasses = (props: {
67+
schoolName: string;
68+
classNames: string[];
69+
}) => {
70+
return e.select(e.Class, (c) => {
71+
const nameMatches = e.op(c.name, "in", e.set(...props.classNames));
72+
const schoolMatches = e.op(c.school.name, "=", props.schoolName);
73+
74+
return {
75+
filter: e.op(nameMatches, "and", schoolMatches),
76+
name: true,
77+
};
78+
});
79+
};

src/utils/strings/split.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* Split a string at commas but not escaped ones
3+
*/
4+
export const split = (str: string) =>
5+
str.split(/(?<!\\),/).map((s) => s.replace(/\\,/g, ","));

tests/array.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { describe, expect, it } from "bun:test";
2+
import { removeDuplicates } from "utils/arrays/duplicates";
3+
import { areSameValue } from "utils/arrays/general";
4+
import { merge } from "utils/arrays/merge";
5+
6+
describe("merge", () => {
7+
it("same length", () => {
8+
const result = merge(
9+
{ key: "a", array: [1, 2, 3] },
10+
{ key: "b", array: ["x", "y", "z"] },
11+
);
12+
13+
expect(result).toEqual([
14+
{ a: 1, b: "x" },
15+
{ a: 2, b: "y" },
16+
{ a: 3, b: "z" },
17+
]);
18+
});
19+
20+
it("works when the first one is longer", () => {
21+
const result = merge(
22+
{ key: "a", array: [1, 2, 3] },
23+
{ key: "b", array: ["a"] },
24+
);
25+
26+
expect(result).toEqual([
27+
{ a: 1, b: "a" },
28+
{ a: 2, b: undefined },
29+
{ a: 3, b: undefined },
30+
]);
31+
});
32+
33+
it("works when the second one is longer", () => {
34+
const result = merge(
35+
{ key: "b", array: ["a"] },
36+
{ key: "a", array: [1, 2, 3] },
37+
);
38+
39+
expect(result).toEqual([
40+
{ b: "a", a: 1 },
41+
{ b: undefined, a: 2 },
42+
{ b: undefined, a: 3 },
43+
]);
44+
});
45+
});
46+
47+
describe("duplicates", () => {
48+
it("does nothing when there'no duplicates", () => {
49+
expect(removeDuplicates([1, 2, 3, 4])).toEqual([1, 2, 3, 4]);
50+
expect(removeDuplicates(["a", "b", "c"])).toEqual(["a", "b", "c"]);
51+
});
52+
53+
it("removes duplicates", () => {
54+
expect(removeDuplicates([1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1])).toEqual([
55+
1, 2,
56+
]);
57+
expect(
58+
removeDuplicates(["ts", "rust", "linux", "i3", "linux", "js", "rust"]),
59+
).toEqual(["ts", "rust", "linux", "i3", "js"]);
60+
});
61+
});
62+
63+
describe("same value", () => {
64+
it("works for equal arrays", () => {
65+
const testCases = [[], ["a", "a"], ["a", "b"], [1, 2], [1, 1]];
66+
67+
for (const tCase of testCases) {
68+
// @ts-ignore
69+
expect(areSameValue(tCase, tCase)).toBeTrue();
70+
}
71+
});
72+
});

tests/strings.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { describe, expect, it } from "bun:test";
2+
import { split } from "utils/strings/split";
3+
4+
describe("split", () => {
5+
it("splits at ,", () => {
6+
const original = "value,value2,valu5,";
7+
expect(split(original)).toEqual(["value", "value2", "valu5", ""]);
8+
expect(original).toBe("value,value2,valu5,");
9+
});
10+
11+
it("ignores escaped commas", () => {
12+
const original = "this\\,is\\,;one,and\\,this\\,another";
13+
expect(split(original)).toEqual(["this,is,;one", "and,this,another"]);
14+
});
15+
});

0 commit comments

Comments
 (0)