Skip to content
This repository has been archived by the owner on Oct 18, 2024. It is now read-only.

feat: ✨ add filtering by restriction code to WebSoc endpoint #130

Merged
merged 14 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions apps/api/src/routes/v1/graphql/schema/enum.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,29 @@ enum SectionType {
Tap
Tut
}

"The set of valid restriction codes."
enum RestrictionCode {
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
R
S
X
}

"The set of valid options for filtering full courses."
enum FullCourses {
ANY
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/routes/v1/graphql/schema/websoc.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ extend type Query {
maxCapacity: String
fullCourses: FullCourses
cancelledCourses: CancelledCourses
excludeRestrictionCodes: [RestrictionCode!]
): WebsocAPIResponse!
"Get data on all available departments."
depts: [Department!]!
Expand Down
34 changes: 32 additions & 2 deletions apps/api/src/routes/v1/rest/websoc/+endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { QuerySchema } from "./schema";

const prisma = new PrismaClient();

// let connected = false
const lambdaClient = await APILambdaClient.new();

async function onWarm() {
Expand Down Expand Up @@ -111,7 +110,13 @@ export const GET = createHandler(async (event, context, res) => {
queries: normalizeQuery(parsedQuery),
});

return res.createOKResult(websocResults, headers, requestId);
if (!parsedQuery.excludeRestrictionCodes?.length) {
return res.createOKResult(websocResults, headers, requestId);
}

const filteredWebsocResults = filterResults(websocResults, parsedQuery.excludeRestrictionCodes);

return res.createOKResult(filteredWebsocResults, headers, requestId);
} catch (error) {
if (error instanceof ZodError) {
const messages = error.issues.map((issue) => issue.message);
Expand All @@ -120,3 +125,28 @@ export const GET = createHandler(async (event, context, res) => {
return res.createErrorResult(400, error, requestId);
}
}, onWarm);

function filterResults(results: WebsocAPIResponse, restrictionCodes: string[]): WebsocAPIResponse {
results.schools = results.schools
.map((school) => {
school.departments = school.departments
.map((department) => {
department.courses = department.courses
.map((course) => {
course.sections = course.sections.filter(
(section) =>
!section.restrictions
.split(/ and | or /)
.some((code: string) => restrictionCodes.includes(code)),
);
return course;
})
.filter((course) => course.sections.length > 0);
return department;
})
.filter((department) => department.courses.length > 0);
return school;
})
.filter((school) => school.departments.length > 0);
return results;
}
11 changes: 11 additions & 0 deletions apps/api/src/routes/v1/rest/websoc/lib.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { $Enums } from "@libs/db";
import type { Prisma } from "@libs/db";
import type { WebsocAPIOptions } from "@libs/uc-irvine-lib/websoc";
import { notNull } from "@libs/utils";
Expand Down Expand Up @@ -200,6 +201,16 @@ export function constructPrismaQuery(parsedQuery: Query): Prisma.WebsocSectionWh
);
}

if (parsedQuery.excludeRestrictionCodes) {
AND.push({
NOT: {
restrictionCodes: {
hasSome: parsedQuery.excludeRestrictionCodes as $Enums.RestrictionCode[],
},
},
});
}

return {
AND,

Expand Down
23 changes: 22 additions & 1 deletion apps/api/src/routes/v1/rest/websoc/schema.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { $Enums } from "@libs/db";
import {
anyArray,
cancelledCoursesOptions,
Expand Down Expand Up @@ -51,6 +52,12 @@ export const QuerySchema = z
units: z.string().array().or(z.string()).optional().transform(flattenStringsAndSplit),
startTime: z.optional(z.literal("").or(z.string().regex(/([1-9]|1[0-2]):[0-5][0-9][ap]m/))),
endTime: z.optional(z.literal("").or(z.string().regex(/([1-9]|1[0-2]):[0-5][0-9][ap]m/))),
excludeRestrictionCodes: z
.string()
.array()
.or(z.string())
.optional()
.transform(flattenStringsAndSplit),
})
.refine((x) => x.cache || !x.cacheOnly, {
message: "cacheOnly cannot be true if cache is false",
Expand All @@ -67,7 +74,21 @@ export const QuerySchema = z
)
.refine((x) => x.cacheOnly || x.building || !x.room, {
message: 'If "building" is provided, "room" must also be provided',
});
})
.refine(
(x) => {
// If not excluding restriction codes, then no more validation is needed.
if (x.excludeRestrictionCodes == null) return true;

// Ensure that all provided restriction codes are valid.
return x.excludeRestrictionCodes.every((code) =>
Object.values($Enums.RestrictionCode).includes(code as $Enums.RestrictionCode),
);
},
{
message: `Restriction codes must be in [${Object.values($Enums.RestrictionCode).join(", ")}]`,
},
);

/**
* Type of the parsed query: useful for passing the query as input to other functions.
Expand Down
60 changes: 41 additions & 19 deletions libs/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,27 @@ enum WebsocSectionType {
Tut
}

enum RestrictionCode {
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
R
S
X
}

// Models

model CalendarTerm {
Expand Down Expand Up @@ -224,25 +245,26 @@ model WebsocSectionMeeting {
}

model WebsocSection {
year String
quarter Quarter
sectionCode Int
timestamp DateTime
geCategories Json
department String
courseNumber String
courseNumeric Int
instructors WebsocSectionInstructor[]
courseTitle String
sectionType WebsocSectionType
units String
meetings WebsocSectionMeeting[]
maxCapacity Int
sectionFull Boolean
waitlistFull Boolean
overEnrolled Boolean
cancelled Boolean
data Json
year String
quarter Quarter
sectionCode Int
timestamp DateTime
geCategories Json
department String
courseNumber String
courseNumeric Int
instructors WebsocSectionInstructor[]
courseTitle String
sectionType WebsocSectionType
units String
meetings WebsocSectionMeeting[]
maxCapacity Int
sectionFull Boolean
waitlistFull Boolean
overEnrolled Boolean
cancelled Boolean
restrictionCodes RestrictionCode[]
data Json

@@id([year, quarter, sectionCode, timestamp])
@@unique([year, quarter, sectionCode, timestamp], name: "idx")
Expand Down
5 changes: 5 additions & 0 deletions services/websoc-scraper-v2/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { $Enums } from "@libs/db";
import { PrismaClient } from "@libs/db";
import { getTermDateData } from "@libs/uc-irvine-lib/registrar";
import type {
Expand Down Expand Up @@ -116,6 +117,7 @@ type ProcessedSection = {
waitlistFull: boolean;
overEnrolled: boolean;
cancelled: boolean;
restrictionCodes: $Enums.RestrictionCode[];
data: object;
};
};
Expand Down Expand Up @@ -391,6 +393,9 @@ async function scrape(name: string, term: Term) {
parseInt(section.numCurrentlyEnrolled.totalEnrolled, 10) >
parseInt(section.maxCapacity, 10),
cancelled: section.sectionComment.includes("*** CANCELLED ***"),
restrictionCodes: section.restrictions
? (section.restrictions.split(/ and | or /) as $Enums.RestrictionCode[])
: [],
data: isolateSection({ school, department, course, section }),
},
};
Expand Down
Loading