Skip to content

Commit

Permalink
Add an endpoint to retrieve assignments
Browse files Browse the repository at this point in the history
  • Loading branch information
Dlurak committed Mar 31, 2024
1 parent c386b92 commit 7f8d479
Show file tree
Hide file tree
Showing 10 changed files with 290 additions and 3 deletions.
7 changes: 4 additions & 3 deletions src/routes/assignments/index.ts
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);
122 changes: 122 additions & 0 deletions src/routes/assignments/list.ts
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 }),
}),
},
);
8 changes: 8 additions & 0 deletions src/utils/arrays/duplicates.ts
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);
};
4 changes: 4 additions & 0 deletions src/utils/arrays/filter.ts
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);
11 changes: 11 additions & 0 deletions src/utils/arrays/general.ts
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));
};
34 changes: 34 additions & 0 deletions src/utils/arrays/merge.ts
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;
};
15 changes: 15 additions & 0 deletions src/utils/db/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,18 @@ export async function doesClassExist(props: ClassIdentifier) {

return !!result.data;
}

export const multipleClasses = (props: {
schoolName: string;
classNames: string[];
}) => {
return e.select(e.Class, (c) => {
const nameMatches = e.op(c.name, "in", e.set(...props.classNames));
const schoolMatches = e.op(c.school.name, "=", props.schoolName);

return {
filter: e.op(nameMatches, "and", schoolMatches),
name: true,
};
});
};
5 changes: 5 additions & 0 deletions src/utils/strings/split.ts
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, ","));
72 changes: 72 additions & 0 deletions tests/array.test.ts
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();
}
});
});
15 changes: 15 additions & 0 deletions tests/strings.test.ts
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"]);
});
});

0 comments on commit 7f8d479

Please sign in to comment.