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

Commit

Permalink
chore: merge 'main' into study-room-scraper
Browse files Browse the repository at this point in the history
  • Loading branch information
ecxyzzy committed May 15, 2024
2 parents 102d73b + 74889e2 commit c05c145
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 33 deletions.
3 changes: 1 addition & 2 deletions apps/api/src/routes/v1/graphql/lib.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { BaseContext, HTTPGraphQLResponse } from "@apollo/server";
import type { IFieldResolver } from "@graphql-tools/utils";
import { isErrorResponse } from "@peterportal-api/types";
import type { RawResponse } from "@peterportal-api/types";
import { GraphQLError } from "graphql/error";

Expand Down Expand Up @@ -58,7 +57,7 @@ export const proxyRestApi =
};
});

if (isErrorResponse(data)) {
if (!data.success) {
throw new GraphQLError(data.message, {
extensions: {
code: data.error.toUpperCase().replace(" ", "_"),
Expand Down
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
8 changes: 4 additions & 4 deletions libs/lambda/src/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function createOKResult<T>(
): APIGatewayProxyResult {
const statusCode = 200;
const timestamp = createTimestamp();
const response: Response<T> = { statusCode, timestamp, requestId, payload };
const response: Response<T> = { statusCode, timestamp, requestId, payload, success: true };
const headers = { ...responseHeaders };

try {
Expand Down Expand Up @@ -89,12 +89,12 @@ export function createErrorResult(
e instanceof Error
? `${e.name}: ${e.message}`
: typeof e === "string"
? e
: "An unknown error has occurred. Please try again.";
? e
: "An unknown error has occurred. Please try again.";

const error = httpErrorCodes[statusCode as keyof typeof httpErrorCodes];

const body: ErrorResponse = { timestamp, requestId, statusCode, error, message };
const body: ErrorResponse = { timestamp, requestId, statusCode, error, message, success: false };

logger.error(`${body.statusCode} ${body.error}: ${body.message}`);

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
},
"packageManager": "[email protected]",
"engines": {
"node": "20",
"pnpm": "8"
}
}
6 changes: 5 additions & 1 deletion packages/types/types/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export type Response<T> = BaseResponse & {
* The payload returned by the REST API.
*/
payload: T;

success: true;
};

/**
Expand All @@ -39,6 +41,8 @@ export type ErrorResponse = BaseResponse & {
* The detailed error message.
*/
message: string;

success: false;
};

/**
Expand All @@ -54,4 +58,4 @@ export type RawResponse<T> = Response<T> | ErrorResponse;
* ``ErrorResponse`` or a ``Response<T>``.
* @param r The object to test.
*/
export const isErrorResponse = <T>(r: RawResponse<T>): r is ErrorResponse => "error" in r;
export const isErrorResponse = <T>(r: RawResponse<T>): r is ErrorResponse => r.success == false;
5 changes: 2 additions & 3 deletions packages/websoc-fuzzy-search/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { normalize } from "node:path";
import { gzipSync } from "node:zlib";

import { isErrorResponse } from "@peterportal-api/types";
import type { Course, Instructor, RawResponse } from "@peterportal-api/types";
import fetch from "cross-fetch";
import pluralize from "pluralize";
Expand Down Expand Up @@ -219,7 +218,7 @@ async function main() {
console.time("Data fetched in");
const coursesRes = await fetch("https://api-next.peterportal.org/v1/rest/courses/all");
const coursesJson: RawResponse<Course[]> = await coursesRes.json();
if (isErrorResponse(coursesJson)) throw new Error("Could not fetch courses from API.");
if (!coursesJson.success) throw new Error("Could not fetch courses from API.");
coursesJson.payload.forEach(
({ id, department, departmentName, courseNumber, geList, courseLevel, school, title }) => {
d.courses[id] = {
Expand All @@ -235,7 +234,7 @@ async function main() {
);
const instructorsRes = await fetch("https://api-next.peterportal.org/v1/rest/instructors/all");
const instructorsJson: RawResponse<Instructor[]> = await instructorsRes.json();
if (isErrorResponse(instructorsJson)) throw new Error("Could not fetch instructors from API.");
if (!instructorsJson.success) throw new Error("Could not fetch instructors from API.");
instructorsJson.payload.forEach(({ ucinetid, shortenedName, name, schools, department }) => {
d.instructors[ucinetid] = {
ucinetid,
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

0 comments on commit c05c145

Please sign in to comment.