From 0bdcd2c08c7508daf3317d56694c53200dddb004 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Sun, 10 Nov 2024 18:43:35 -0800 Subject: [PATCH 01/34] feat: initial --- apps/antalmanac/package.json | 2 +- apps/antalmanac/src/lib/grades.ts | 4 +- apps/backend/.env.sample | 2 +- apps/backend/package.json | 8 +- apps/backend/src/routers/course.ts | 16 + apps/backend/src/routers/index.ts | 2 + apps/cdk/package.json | 2 +- .../.eslintrc.yml | 0 packages/anteater-api-schemas/.gitignore | 1 + packages/anteater-api-schemas/package.json | 19 + packages/anteater-api-schemas/src/calendar.ts | 15 + packages/anteater-api-schemas/src/courses.ts | 34 ++ .../src/enumerate.ts | 2 +- packages/anteater-api-schemas/src/grades.ts | 46 ++ packages/anteater-api-schemas/src/index.ts | 5 + .../anteater-api-schemas/src/instructor.ts | 20 + .../src/websoc.ts | 0 .../tsconfig.json | 0 packages/peterportal-schemas/package.json | 17 - packages/peterportal-schemas/src/calendar.ts | 15 - packages/peterportal-schemas/src/courses.ts | 43 -- packages/peterportal-schemas/src/grades.ts | 46 -- packages/peterportal-schemas/src/index.ts | 5 - .../peterportal-schemas/src/instructor.ts | 20 - packages/types/package.json | 4 +- packages/types/src/websoc.ts | 2 +- pnpm-lock.yaml | 416 ++++++++++++------ 27 files changed, 452 insertions(+), 294 deletions(-) create mode 100644 apps/backend/src/routers/course.ts rename packages/{peterportal-schemas => anteater-api-schemas}/.eslintrc.yml (100%) create mode 100644 packages/anteater-api-schemas/.gitignore create mode 100644 packages/anteater-api-schemas/package.json create mode 100644 packages/anteater-api-schemas/src/calendar.ts create mode 100644 packages/anteater-api-schemas/src/courses.ts rename packages/{peterportal-schemas => anteater-api-schemas}/src/enumerate.ts (56%) create mode 100644 packages/anteater-api-schemas/src/grades.ts create mode 100644 packages/anteater-api-schemas/src/index.ts create mode 100644 packages/anteater-api-schemas/src/instructor.ts rename packages/{peterportal-schemas => anteater-api-schemas}/src/websoc.ts (100%) rename packages/{peterportal-schemas => anteater-api-schemas}/tsconfig.json (100%) delete mode 100644 packages/peterportal-schemas/package.json delete mode 100644 packages/peterportal-schemas/src/calendar.ts delete mode 100644 packages/peterportal-schemas/src/courses.ts delete mode 100644 packages/peterportal-schemas/src/grades.ts delete mode 100644 packages/peterportal-schemas/src/index.ts delete mode 100644 packages/peterportal-schemas/src/instructor.ts diff --git a/apps/antalmanac/package.json b/apps/antalmanac/package.json index ee1079e93..09cc8b383 100644 --- a/apps/antalmanac/package.json +++ b/apps/antalmanac/package.json @@ -98,7 +98,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "peterportal-api-next-types": "1.0.0-rc.2.68.0", "prettier": "^2.8.4", - "typescript": "^4.9.5", + "typescript": "5.6.3", "vite": "^4.4.9", "vite-plugin-svgr": "^2.4.0" } diff --git a/apps/antalmanac/src/lib/grades.ts b/apps/antalmanac/src/lib/grades.ts index 597654b8e..c6a4e06c3 100644 --- a/apps/antalmanac/src/lib/grades.ts +++ b/apps/antalmanac/src/lib/grades.ts @@ -35,9 +35,7 @@ export interface GroupedGradesGraphQLResponse { /** * Class to handle querying and caching of grades. - * Retrieves grades from the PeterPortal GraphQL API. - * - * Note: Be careful with sending too many queries to the GraphQL API. It's not very fast and can be DoS'd easily. + * Retrieves grades from Anteater API. */ class _Grades { gradesCache: Record; diff --git a/apps/backend/.env.sample b/apps/backend/.env.sample index c935e3941..b55f343c7 100644 --- a/apps/backend/.env.sample +++ b/apps/backend/.env.sample @@ -11,4 +11,4 @@ USERDATA_TABLE_NAME=tablename AWS_REGION=us-east-1 # For Mapbox API -MAPBOX_ACCESS_TOKEN=pk.abc123 \ No newline at end of file +MAPBOX_ACCESS_TOKEN=pk.abc123 diff --git a/apps/backend/package.json b/apps/backend/package.json index 2a2061e82..03fba78fe 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -10,6 +10,7 @@ "lint": "eslint --fix src" }, "dependencies": { + "@packages/anteater-api-schemas": "workspace:*", "@packages/antalmanac-types": "workspace:*", "@trpc/server": "^10.30.0", "@vendia/serverless-express": "^4.10.1", @@ -22,7 +23,7 @@ "mongodb": "^5.0.1", "mongoose": "^7.1.0j", "superjson": "^1.12.3", - "websoc-api": "^3.0.0" + "zod": "3.23.8" }, "devDependencies": { "@aws-sdk/client-dynamodb": "^3.332.0", @@ -42,12 +43,11 @@ "nodemon": "^2.0.22", "prettier": "^2.8.4", "tsx": "^3.12.7", - "typescript": "^4.9.5" + "typescript": "5.6.3" }, "lint-staged": { "*.{js,json,css,html}": [ - "prettier --write", - "git add" + "prettier --write" ] } } diff --git a/apps/backend/src/routers/course.ts b/apps/backend/src/routers/course.ts new file mode 100644 index 000000000..673d6fe92 --- /dev/null +++ b/apps/backend/src/routers/course.ts @@ -0,0 +1,16 @@ +import {z} from "zod"; +import {procedure, router} from "../trpc"; + +const courseRouter = router({ + get: procedure + .input(z.object({ id: z.string() })) + .query(async ({ input }) => { + const res = await fetch(`https://anteaterapi.com/v2/rest/courses/${encodeURIComponent(input.id)}`, { + headers: { + ...process.env.ANTEATER_API_KEY && { Authorization: `Bearer ${process.env.ANTEATER_API_KEY}`} + } + }).then(data => data.json()).then(data => data.ok ? data.data as Course : null); + }) +}); + +export default courseRouter; diff --git a/apps/backend/src/routers/index.ts b/apps/backend/src/routers/index.ts index 2611cca4a..3ef1c8b08 100644 --- a/apps/backend/src/routers/index.ts +++ b/apps/backend/src/routers/index.ts @@ -2,8 +2,10 @@ import { router } from '../trpc'; import newsRouter from './news'; import usersRouter from './users'; import zotcourseRouter from "./zotcours"; +import courseRouter from "./course"; const appRouter = router({ + course: courseRouter, news: newsRouter, users: usersRouter, zotcourse: zotcourseRouter diff --git a/apps/cdk/package.json b/apps/cdk/package.json index 5fd034c38..36f2f503d 100644 --- a/apps/cdk/package.json +++ b/apps/cdk/package.json @@ -30,7 +30,7 @@ "@types/node": "^20.11.5", "aws-cdk": "^2.94.0", "tsx": "^3.12.7", - "typescript": "^5.1.0" + "typescript": "5.6.3" }, "packageManager": "pnpm@8.6.12", "engines": { diff --git a/packages/peterportal-schemas/.eslintrc.yml b/packages/anteater-api-schemas/.eslintrc.yml similarity index 100% rename from packages/peterportal-schemas/.eslintrc.yml rename to packages/anteater-api-schemas/.eslintrc.yml diff --git a/packages/anteater-api-schemas/.gitignore b/packages/anteater-api-schemas/.gitignore new file mode 100644 index 000000000..d3db8f9b1 --- /dev/null +++ b/packages/anteater-api-schemas/.gitignore @@ -0,0 +1 @@ +src/generated/ diff --git a/packages/anteater-api-schemas/package.json b/packages/anteater-api-schemas/package.json new file mode 100644 index 000000000..a4d27adab --- /dev/null +++ b/packages/anteater-api-schemas/package.json @@ -0,0 +1,19 @@ +{ + "name": "@packages/anteater-api-schemas", + "version": "0.0.1", + "description": "Internal Anteater API ArkType schemas for AntAlmanac", + "main": "./src/index.ts", + "types": "./src/index.ts", + "type": "module", + "scripts": { + "postinstall": "openapi-typescript https://anteaterapi.com/openapi.json -o src/generated/anteater-api-types.ts" + }, + "dependencies": { + "arktype": "1.0.14-alpha", + "peterportal-api-next-types": "1.0.0-alpha.6" + }, + "devDependencies": { + "typescript": "5.6.3", + "openapi-typescript": "7.4.3" + } +} diff --git a/packages/anteater-api-schemas/src/calendar.ts b/packages/anteater-api-schemas/src/calendar.ts new file mode 100644 index 000000000..093a591d8 --- /dev/null +++ b/packages/anteater-api-schemas/src/calendar.ts @@ -0,0 +1,15 @@ +import { type Infer, type } from 'arktype'; +import type { Quarter } from 'peterportal-api-next-types'; + +export const WeekData = type({ + week: 'string', + quarter: 'string' as Infer<`${string} ${Quarter}`>, + display: 'string', +}); + +export const QuarterDates = type({ + instructionStart: 'Date', + instructionEnd: 'Date', + finalsStart: 'Date', + finalsEnd: 'Date', +}); diff --git a/packages/anteater-api-schemas/src/courses.ts b/packages/anteater-api-schemas/src/courses.ts new file mode 100644 index 000000000..56040cb12 --- /dev/null +++ b/packages/anteater-api-schemas/src/courses.ts @@ -0,0 +1,34 @@ +import { paths } from './generated/anteater-api-types'; + +type _Course = paths['/v2/rest/courses/{id}']['get']['responses'][200]['content']['application/json']['data']; + +type CoursePrerequisite = { + prereqType: 'course'; + coreq: false; + courseId: string; + minGrade?: string; +}; + +type CourseCorequisite = { + prereqType: 'course'; + coreq: true; + courseId: string; +}; + +type ExamPrerequisite = { + prereqType: 'exam'; + examName: string; + minGrade?: string; +}; + +export type Prerequisite = CoursePrerequisite | CourseCorequisite | ExamPrerequisite; + +export type PrerequisiteTree = { + AND?: Array; + OR?: Array; + NOT?: Array; +}; + +export interface Course extends _Course { + prerequisiteTree: PrerequisiteTree; +} diff --git a/packages/peterportal-schemas/src/enumerate.ts b/packages/anteater-api-schemas/src/enumerate.ts similarity index 56% rename from packages/peterportal-schemas/src/enumerate.ts rename to packages/anteater-api-schemas/src/enumerate.ts index f9bf7e3bc..5bb1a0e80 100644 --- a/packages/peterportal-schemas/src/enumerate.ts +++ b/packages/anteater-api-schemas/src/enumerate.ts @@ -1,5 +1,5 @@ function enumerate(values: T) { - return values.map((v) => `"${v}"`).join("|") as `"${T[number]}"`; + return values.map((v) => `"${v}"`).join('|') as `"${T[number]}"`; } export default enumerate; diff --git a/packages/anteater-api-schemas/src/grades.ts b/packages/anteater-api-schemas/src/grades.ts new file mode 100644 index 000000000..3ee994f8b --- /dev/null +++ b/packages/anteater-api-schemas/src/grades.ts @@ -0,0 +1,46 @@ +import { arrayOf, type } from 'arktype'; +import { quarters } from 'peterportal-api-next-types'; +import enumerate from './enumerate'; + +/** + * A section which has grades data associated with it. + */ +export const GradeSection = type({ + year: 'string', + quarter: enumerate(quarters), + department: 'string', + courseNumber: 'string', + courseNumeric: 'number', + sectionCode: 'string', + instructors: 'string[]', +}); + +export const GradeDistribution = type({ + gradeACount: 'number', + gradeBCount: 'number', + gradeCCount: 'number', + gradeDCount: 'number', + gradeFCount: 'number', + gradePCount: 'number', + gradeNPCount: 'number', + gradeWCount: 'number', + averageGPA: 'number', +}); + +/** + * The type of the payload returned on a successful response from querying + * ``/v1/rest/grades/raw``. + * @alpha + */ +export const GradesRaw = arrayOf(type([GradeSection, '&', GradeDistribution])); + +/** + * An object that represents aggregate grades statistics for a given query. + * The type of the payload returned on a successful response from querying + * ``/v1/rest/grades/aggregate``. + * @alpha + */ +export const GradesAggregate = type({ + sectionList: arrayOf(GradeSection), + gradeDistribution: GradeDistribution, +}); diff --git a/packages/anteater-api-schemas/src/index.ts b/packages/anteater-api-schemas/src/index.ts new file mode 100644 index 000000000..8f22c1c4b --- /dev/null +++ b/packages/anteater-api-schemas/src/index.ts @@ -0,0 +1,5 @@ +export * from './calendar'; +export * from './courses'; +export * from './grades'; +export * from './instructor'; +export * from './websoc'; diff --git a/packages/anteater-api-schemas/src/instructor.ts b/packages/anteater-api-schemas/src/instructor.ts new file mode 100644 index 000000000..07ffb2561 --- /dev/null +++ b/packages/anteater-api-schemas/src/instructor.ts @@ -0,0 +1,20 @@ +import { arrayOf, type } from 'arktype'; + +/** + * An object representing an instructor. + * The type of the payload returned on a successful response from querying + * ``/v1/rest/instructors/{ucinetid}``. + * @alpha + */ +export const Instructor = type({ + ucinetid: 'string', + instructorName: 'string', + shortenedName: 'string', + title: 'string', + department: 'string', + schools: 'string[]', + relatedDepartments: 'string[]', + courseHistory: 'string[]', +}); + +export const Instructors = arrayOf(Instructor); diff --git a/packages/peterportal-schemas/src/websoc.ts b/packages/anteater-api-schemas/src/websoc.ts similarity index 100% rename from packages/peterportal-schemas/src/websoc.ts rename to packages/anteater-api-schemas/src/websoc.ts diff --git a/packages/peterportal-schemas/tsconfig.json b/packages/anteater-api-schemas/tsconfig.json similarity index 100% rename from packages/peterportal-schemas/tsconfig.json rename to packages/anteater-api-schemas/tsconfig.json diff --git a/packages/peterportal-schemas/package.json b/packages/peterportal-schemas/package.json deleted file mode 100644 index a455ea574..000000000 --- a/packages/peterportal-schemas/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@packages/peterportal-schemas", - "version": "0.0.1", - "description": "Internal PeterPortal API ArkType schemas for AntAlmanac", - "main": "./src/index.ts", - "types": "./src/index.ts", - "type": "module", - "scripts": { - }, - "dependencies": { - "arktype": "1.0.14-alpha", - "peterportal-api-next-types": "1.0.0-alpha.6" - }, - "devDependencies": { - "typescript": "^4.9" - } -} diff --git a/packages/peterportal-schemas/src/calendar.ts b/packages/peterportal-schemas/src/calendar.ts deleted file mode 100644 index 8435f4cb4..000000000 --- a/packages/peterportal-schemas/src/calendar.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { type Infer, type } from "arktype"; -import type { Quarter } from "peterportal-api-next-types"; - -export const WeekData = type({ - week: "string", - quarter: "string" as Infer<`${string} ${Quarter}`>, - display: "string", -}); - -export const QuarterDates = type({ - instructionStart: "Date", - instructionEnd: "Date", - finalsStart: "Date", - finalsEnd: "Date", -}); diff --git a/packages/peterportal-schemas/src/courses.ts b/packages/peterportal-schemas/src/courses.ts deleted file mode 100644 index 636b91dc5..000000000 --- a/packages/peterportal-schemas/src/courses.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { type } from "arktype"; -import { courseLevels, geCategories } from "peterportal-api-next-types"; -import enumerate from "./enumerate"; - -export const PrerequisiteTree = type({ - "AND?": "string[]", - "OR?": "string[]", -}); - -/** - * An object that represents a course. - * The type of the payload returned on a successful response from querying - * ``/v1/rest/courses/{courseId}``. - * @alpha - */ -export const Course = type({ - id: "string", - department: "string", - courseNumber: "string", - courseNumeric: "number", - school: "string", - title: "string", - courseLevel: enumerate(courseLevels), - minUnits: "string", - maxUnits: "string", - description: "string", - departmentName: "string", - instructorHistory: "string[]", - prerequisiteTree: PrerequisiteTree, - prerequisiteList: "string[]", - prerequisiteText: "string", - prerequisiteFor: "string[]", - repeatability: "string", - gradingOption: "string", - concurrent: "string", - sameAs: "string", - restriction: "string", - overlap: "string", - corequisite: "string", - geList: enumerate(geCategories), - geText: "string", - terms: "string[]", -}); diff --git a/packages/peterportal-schemas/src/grades.ts b/packages/peterportal-schemas/src/grades.ts deleted file mode 100644 index 70ceeb385..000000000 --- a/packages/peterportal-schemas/src/grades.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { arrayOf, type } from "arktype"; -import { quarters } from "peterportal-api-next-types"; -import enumerate from "./enumerate"; - -/** - * A section which has grades data associated with it. - */ -export const GradeSection = type({ - year: "string", - quarter: enumerate(quarters), - department: "string", - courseNumber: "string", - courseNumeric: "number", - sectionCode: "string", - instructors: "string[]", -}); - -export const GradeDistribution = type({ - gradeACount: "number", - gradeBCount: "number", - gradeCCount: "number", - gradeDCount: "number", - gradeFCount: "number", - gradePCount: "number", - gradeNPCount: "number", - gradeWCount: "number", - averageGPA: "number", -}); - -/** - * The type of the payload returned on a successful response from querying - * ``/v1/rest/grades/raw``. - * @alpha - */ -export const GradesRaw = arrayOf(type([GradeSection, "&", GradeDistribution])); - -/** - * An object that represents aggregate grades statistics for a given query. - * The type of the payload returned on a successful response from querying - * ``/v1/rest/grades/aggregate``. - * @alpha - */ -export const GradesAggregate = type({ - sectionList: arrayOf(GradeSection), - gradeDistribution: GradeDistribution, -}); diff --git a/packages/peterportal-schemas/src/index.ts b/packages/peterportal-schemas/src/index.ts deleted file mode 100644 index b73e17c37..000000000 --- a/packages/peterportal-schemas/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./calendar"; -export * from "./courses"; -export * from "./grades"; -export * from "./instructor"; -export * from "./websoc"; diff --git a/packages/peterportal-schemas/src/instructor.ts b/packages/peterportal-schemas/src/instructor.ts deleted file mode 100644 index f93943c70..000000000 --- a/packages/peterportal-schemas/src/instructor.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { arrayOf, type } from "arktype"; - -/** - * An object representing an instructor. - * The type of the payload returned on a successful response from querying - * ``/v1/rest/instructors/{ucinetid}``. - * @alpha - */ -export const Instructor = type({ - ucinetid: "string", - instructorName: "string", - shortenedName: "string", - title: "string", - department: "string", - schools: "string[]", - relatedDepartments: "string[]", - courseHistory: "string[]", -}); - -export const Instructors = arrayOf(Instructor); diff --git a/packages/types/package.json b/packages/types/package.json index dddbc4423..dcab957a5 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -9,9 +9,9 @@ }, "dependencies": { "arktype": "1.0.14-alpha", - "@packages/peterportal-schemas": "workspace:*" + "@packages/anteater-api-schemas": "workspace:*" }, "devDependencies": { - "typescript": "^4.9" + "typescript": "5.6.3" } } diff --git a/packages/types/src/websoc.ts b/packages/types/src/websoc.ts index 405a6664a..c1ca181c4 100644 --- a/packages/types/src/websoc.ts +++ b/packages/types/src/websoc.ts @@ -2,7 +2,7 @@ import { arrayOf, type } from 'arktype'; import { WebsocSection as WebsocSectionSchema, WebsocCourse as WebsocCourseSchema, -} from '@packages/peterportal-schemas'; +} from '@packages/anteater-api-schemas'; const AASectionExtendedProperties = type({ color: 'string', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c520bb49d..cd66485d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -283,8 +283,8 @@ importers: specifier: ^2.8.4 version: 2.8.4 typescript: - specifier: ^4.9.5 - version: 4.9.5 + specifier: 5.6.3 + version: 5.6.3 vite: specifier: ^4.4.9 version: 4.4.9(@types/node@18.13.0) @@ -297,6 +297,9 @@ importers: '@packages/antalmanac-types': specifier: workspace:* version: link:../../packages/types + '@packages/anteater-api-schemas': + specifier: workspace:* + version: link:../../packages/anteater-api-schemas '@trpc/server': specifier: ^10.30.0 version: 10.30.0 @@ -330,9 +333,9 @@ importers: superjson: specifier: ^1.12.3 version: 1.12.3 - websoc-api: - specifier: ^3.0.0 - version: 3.0.0 + zod: + specifier: 3.23.8 + version: 3.23.8 devDependencies: '@aws-sdk/client-dynamodb': specifier: ^3.332.0 @@ -351,10 +354,10 @@ importers: version: 4.17.17 '@typescript-eslint/eslint-plugin': specifier: ^5.52.0 - version: 5.57.1(@typescript-eslint/parser@5.57.1(eslint@8.38.0)(typescript@4.9.5))(eslint@8.38.0)(typescript@4.9.5) + version: 5.57.1(@typescript-eslint/parser@5.57.1(eslint@8.38.0)(typescript@5.6.3))(eslint@8.38.0)(typescript@5.6.3) '@typescript-eslint/parser': specifier: ^5.52.0 - version: 5.57.1(eslint@8.38.0)(typescript@4.9.5) + version: 5.57.1(eslint@8.38.0)(typescript@5.6.3) concurrently: specifier: ^8.0.1 version: 8.0.1 @@ -369,7 +372,7 @@ importers: version: 8.8.0(eslint@8.38.0) eslint-plugin-import: specifier: ^2.27.5 - version: 2.27.5(@typescript-eslint/parser@5.57.1(eslint@8.38.0)(typescript@4.9.5))(eslint-import-resolver-typescript@3.6.1(eslint-plugin-import@2.27.5)(eslint@8.38.0))(eslint@8.38.0) + version: 2.27.5(@typescript-eslint/parser@5.57.1(eslint@8.38.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1(eslint-plugin-import@2.27.5)(eslint@8.38.0))(eslint@8.38.0) husky: specifier: ^8.0.3 version: 8.0.3 @@ -386,8 +389,8 @@ importers: specifier: ^3.12.7 version: 3.12.7 typescript: - specifier: ^4.9.5 - version: 4.9.5 + specifier: 5.6.3 + version: 5.6.3 apps/cdk: dependencies: @@ -420,10 +423,10 @@ importers: specifier: ^3.12.7 version: 3.12.7 typescript: - specifier: ^5.1.0 - version: 5.3.3 + specifier: 5.6.3 + version: 5.6.3 - packages/peterportal-schemas: + packages/anteater-api-schemas: dependencies: arktype: specifier: 1.0.14-alpha @@ -432,22 +435,25 @@ importers: specifier: 1.0.0-alpha.6 version: 1.0.0-alpha.6 devDependencies: + openapi-typescript: + specifier: 7.4.3 + version: 7.4.3(typescript@5.6.3) typescript: - specifier: ^4.9 - version: 4.9.5 + specifier: 5.6.3 + version: 5.6.3 packages/types: dependencies: - '@packages/peterportal-schemas': + '@packages/anteater-api-schemas': specifier: workspace:* - version: link:../peterportal-schemas + version: link:../anteater-api-schemas arktype: specifier: 1.0.14-alpha version: 1.0.14-alpha devDependencies: typescript: - specifier: ^4.9 - version: 4.9.5 + specifier: 5.6.3 + version: 5.6.3 packages: @@ -1802,6 +1808,16 @@ packages: peerDependencies: react: 16.x || 17.x || 18.x + '@redocly/ajv@8.11.2': + resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} + + '@redocly/config@0.16.0': + resolution: {integrity: sha512-t9jnODbUcuANRSl/K4L9nb12V+U5acIHnVSl26NWrtSdDZVtoqUXk2yGFPZzohYf62cCfEQUT8ouJ3bhPfpnJg==} + + '@redocly/openapi-core@1.25.11': + resolution: {integrity: sha512-bH+a8izQz4fnKROKoX3bEU8sQ9rjvEIZOqU6qTmxlhOJ0NsKa5e+LmU18SV0oFeg5YhWQhhEDihXkvKJ1wMMNQ==} + engines: {node: '>=14.19.0', npm: '>=7.0.0'} + '@remix-run/router@1.3.2': resolution: {integrity: sha512-t54ONhl/h75X94SWsHGQ4G/ZrCEguKSRQr7DrjTciJXW0YU1QhlwYeycvK5JgkzlxmvrK7wq1NB/PLtHxoiDcA==} engines: {node: '>=14'} @@ -2395,6 +2411,10 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} + agent-base@7.1.1: + resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} + engines: {node: '>= 14'} + aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} @@ -2402,6 +2422,10 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -2557,6 +2581,9 @@ packages: brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + braces@3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} engines: {node: '>=8'} @@ -2618,6 +2645,9 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + chart.js@4.2.1: resolution: {integrity: sha512-6YbpQ0nt3NovAgOzbkSSeeAQu/3za1319dPUQTXn9WcOpywM8rGKxJHrhS8V8xEkAlk8YhEfjbuAPfUyp6jIsw==} engines: {pnpm: ^7.0.0} @@ -2673,6 +2703,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + colorette@2.0.19: resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} @@ -2804,10 +2837,6 @@ packages: damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} - data-urls@4.0.0: resolution: {integrity: sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==} engines: {node: '>=14'} @@ -3165,10 +3194,6 @@ packages: resolution: {integrity: sha512-CDYeykkle1LiA/uqQyNwYpFbyF6Axec6YapmpUP+/RHWIoR1zKjocdvNaTsxCxZzQ6v9MLXaSYm9Qq0thv0DHg==} hasBin: true - fast-xml-parser@4.2.4: - resolution: {integrity: sha512-fbfMDvgBNIdDJLdLOwacjFAPYt67tr31H9ZhWSm45CDAxvd0I6WTlSOUo7K2P/K5sA5JgMKG64PI3DMcaFdWpQ==} - hasBin: true - fast-xml-parser@4.2.5: resolution: {integrity: sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==} hasBin: true @@ -3176,10 +3201,6 @@ packages: fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} - fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -3216,10 +3237,6 @@ packages: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} - formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} - forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -3366,6 +3383,10 @@ packages: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + https-proxy-agent@7.0.5: + resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} + engines: {node: '>= 14'} + human-signals@3.0.1: resolution: {integrity: sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==} engines: {node: '>=12.20.0'} @@ -3414,6 +3435,10 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + index-to-position@0.1.2: + resolution: {integrity: sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g==} + engines: {node: '>=18'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} @@ -3567,6 +3592,10 @@ packages: resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} engines: {node: '>= 0.6.0'} + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + js-sdsl@4.3.0: resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==} @@ -3601,6 +3630,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -3699,6 +3731,9 @@ packages: lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -3795,6 +3830,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -3887,13 +3926,14 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - - node-fetch@3.3.1: - resolution: {integrity: sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true node-releases@2.0.10: resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} @@ -3987,6 +4027,12 @@ packages: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} + openapi-typescript@7.4.3: + resolution: {integrity: sha512-xTIjMIIOv9kNhsr8JxaC00ucbIY/6ZwuJPJBZMSh5FA2dicZN5uM805DWVJojXdom8YI4AQTavPDPHMx/3g0vQ==} + hasBin: true + peerDependencies: + typescript: ^5.x + optionator@0.9.1: resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} engines: {node: '>= 0.8.0'} @@ -4021,6 +4067,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-json@8.1.0: + resolution: {integrity: sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA==} + engines: {node: '>=18'} + parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} @@ -4081,6 +4131,10 @@ packages: pkg-types@1.0.3: resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + popper.js@1.16.1-lts: resolution: {integrity: sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==} @@ -4309,6 +4363,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} @@ -4577,6 +4635,10 @@ packages: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} + supports-color@9.4.0: + resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==} + engines: {node: '>=12'} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -4640,6 +4702,9 @@ packages: resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} engines: {node: '>=6'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@3.0.0: resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} engines: {node: '>=12'} @@ -4728,6 +4793,10 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} + type-fest@4.26.1: + resolution: {integrity: sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==} + engines: {node: '>=16'} + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -4735,13 +4804,8 @@ packages: typed-array-length@1.0.4: resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} - typescript@4.9.5: - resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} - engines: {node: '>=4.2.0'} - hasBin: true - - typescript@5.3.3: - resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} hasBin: true @@ -4792,6 +4856,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + uri-js-replace@1.0.1: + resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -4911,17 +4978,13 @@ packages: resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} engines: {node: '>=10.13.0'} - web-streams-polyfill@3.2.1: - resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} - engines: {node: '>= 8'} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} - websoc-api@3.0.0: - resolution: {integrity: sha512-CR/o6gfy2PJn01qTNehN+D87qveqoPN7Ye15nI2yka066zIXSdhWa0Wpu9HqyChfKczafZJ7Ry/p52zR9f7idQ==} - websoc-fuzzy-search@1.0.1: resolution: {integrity: sha512-1UlDdT2OvMxVIczNSQzI+vSoojfagbORdwtMQiLAnG1zVLG9Po6x5+VWNysi8w5xoxE2NootQH72HzoenLygDg==} @@ -4941,6 +5004,9 @@ packages: resolution: {integrity: sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==} engines: {node: '>=14'} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} @@ -5020,6 +5086,9 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} @@ -5048,6 +5117,9 @@ packages: resolution: {integrity: sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==} engines: {node: '>=10'} + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zustand@4.3.3: resolution: {integrity: sha512-x2jXq8S0kfLGNwGh87nhRfEc2eZy37tSatpSoSIN+O6HIaBhgQHSONV/F9VNrNcBcKQu/E80K1DeHDYQC/zCrQ==} engines: {node: '>=12.7.0'} @@ -6023,7 +6095,7 @@ snapshots: '@babel/traverse': 7.20.13 '@babel/types': 7.20.7 convert-source-map: 1.9.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.0 @@ -6043,7 +6115,7 @@ snapshots: '@babel/traverse': 7.22.17 '@babel/types': 7.22.17 convert-source-map: 1.9.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -6238,7 +6310,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.18.6 '@babel/parser': 7.20.15 '@babel/types': 7.20.7 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -6253,7 +6325,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.22.16 '@babel/types': 7.22.17 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -6525,7 +6597,7 @@ snapshots: '@eslint/eslintrc@2.0.2': dependencies: ajv: 6.12.6 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) espree: 9.5.1 globals: 13.20.0 ignore: 5.2.4 @@ -6562,7 +6634,7 @@ snapshots: '@humanwhocodes/config-array@0.11.8': dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -6948,6 +7020,32 @@ snapshots: react: 18.2.0 resize-observer-polyfill: 1.5.1 + '@redocly/ajv@8.11.2': + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js-replace: 1.0.1 + + '@redocly/config@0.16.0': {} + + '@redocly/openapi-core@1.25.11(supports-color@9.4.0)': + dependencies: + '@redocly/ajv': 8.11.2 + '@redocly/config': 0.16.0 + colorette: 1.4.0 + https-proxy-agent: 7.0.5(supports-color@9.4.0) + js-levenshtein: 1.1.6 + js-yaml: 4.1.0 + lodash.isequal: 4.5.0 + minimatch: 5.1.6 + node-fetch: 2.7.0 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - encoding + - supports-color + '@remix-run/router@1.3.2': {} '@restart/hooks@0.4.9(react@18.2.0)': @@ -7545,34 +7643,34 @@ snapshots: '@types/node': 20.11.5 '@types/webidl-conversions': 7.0.0 - '@typescript-eslint/eslint-plugin@5.57.1(@typescript-eslint/parser@5.57.1(eslint@8.38.0)(typescript@4.9.5))(eslint@8.38.0)(typescript@4.9.5)': + '@typescript-eslint/eslint-plugin@5.57.1(@typescript-eslint/parser@5.57.1(eslint@8.38.0)(typescript@5.6.3))(eslint@8.38.0)(typescript@5.6.3)': dependencies: '@eslint-community/regexpp': 4.5.0 - '@typescript-eslint/parser': 5.57.1(eslint@8.38.0)(typescript@4.9.5) + '@typescript-eslint/parser': 5.57.1(eslint@8.38.0)(typescript@5.6.3) '@typescript-eslint/scope-manager': 5.57.1 - '@typescript-eslint/type-utils': 5.57.1(eslint@8.38.0)(typescript@4.9.5) - '@typescript-eslint/utils': 5.57.1(eslint@8.38.0)(typescript@4.9.5) - debug: 4.3.4 + '@typescript-eslint/type-utils': 5.57.1(eslint@8.38.0)(typescript@5.6.3) + '@typescript-eslint/utils': 5.57.1(eslint@8.38.0)(typescript@5.6.3) + debug: 4.3.4(supports-color@9.4.0) eslint: 8.38.0 grapheme-splitter: 1.0.4 ignore: 5.2.4 natural-compare-lite: 1.4.0 semver: 7.3.8 - tsutils: 3.21.0(typescript@4.9.5) + tsutils: 3.21.0(typescript@5.6.3) optionalDependencies: - typescript: 4.9.5 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@5.57.1(eslint@8.38.0)(typescript@4.9.5)': + '@typescript-eslint/parser@5.57.1(eslint@8.38.0)(typescript@5.6.3)': dependencies: '@typescript-eslint/scope-manager': 5.57.1 '@typescript-eslint/types': 5.57.1 - '@typescript-eslint/typescript-estree': 5.57.1(typescript@4.9.5) - debug: 4.3.4 + '@typescript-eslint/typescript-estree': 5.57.1(typescript@5.6.3) + debug: 4.3.4(supports-color@9.4.0) eslint: 8.38.0 optionalDependencies: - typescript: 4.9.5 + typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -7581,42 +7679,42 @@ snapshots: '@typescript-eslint/types': 5.57.1 '@typescript-eslint/visitor-keys': 5.57.1 - '@typescript-eslint/type-utils@5.57.1(eslint@8.38.0)(typescript@4.9.5)': + '@typescript-eslint/type-utils@5.57.1(eslint@8.38.0)(typescript@5.6.3)': dependencies: - '@typescript-eslint/typescript-estree': 5.57.1(typescript@4.9.5) - '@typescript-eslint/utils': 5.57.1(eslint@8.38.0)(typescript@4.9.5) - debug: 4.3.4 + '@typescript-eslint/typescript-estree': 5.57.1(typescript@5.6.3) + '@typescript-eslint/utils': 5.57.1(eslint@8.38.0)(typescript@5.6.3) + debug: 4.3.4(supports-color@9.4.0) eslint: 8.38.0 - tsutils: 3.21.0(typescript@4.9.5) + tsutils: 3.21.0(typescript@5.6.3) optionalDependencies: - typescript: 4.9.5 + typescript: 5.6.3 transitivePeerDependencies: - supports-color '@typescript-eslint/types@5.57.1': {} - '@typescript-eslint/typescript-estree@5.57.1(typescript@4.9.5)': + '@typescript-eslint/typescript-estree@5.57.1(typescript@5.6.3)': dependencies: '@typescript-eslint/types': 5.57.1 '@typescript-eslint/visitor-keys': 5.57.1 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.3.8 - tsutils: 3.21.0(typescript@4.9.5) + tsutils: 3.21.0(typescript@5.6.3) optionalDependencies: - typescript: 4.9.5 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@5.57.1(eslint@8.38.0)(typescript@4.9.5)': + '@typescript-eslint/utils@5.57.1(eslint@8.38.0)(typescript@5.6.3)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.38.0) '@types/json-schema': 7.0.11 '@types/semver': 7.3.13 '@typescript-eslint/scope-manager': 5.57.1 '@typescript-eslint/types': 5.57.1 - '@typescript-eslint/typescript-estree': 5.57.1(typescript@4.9.5) + '@typescript-eslint/typescript-estree': 5.57.1(typescript@5.6.3) eslint: 8.38.0 eslint-scope: 5.1.1 semver: 7.3.8 @@ -7690,7 +7788,13 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) + transitivePeerDependencies: + - supports-color + + agent-base@7.1.1(supports-color@9.4.0): + dependencies: + debug: 4.3.4(supports-color@9.4.0) transitivePeerDependencies: - supports-color @@ -7706,6 +7810,8 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-colors@4.1.3: {} + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -7877,6 +7983,10 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + braces@3.0.2: dependencies: fill-range: 7.0.1 @@ -7943,6 +8053,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + change-case@5.4.4: {} + chart.js@4.2.1: dependencies: '@kurkle/color': 0.3.2 @@ -8001,6 +8113,8 @@ snapshots: color-name@1.1.4: {} + colorette@1.4.0: {} + colorette@2.0.19: {} combined-stream@1.0.8: @@ -8127,8 +8241,6 @@ snapshots: damerau-levenshtein@1.0.8: {} - data-uri-to-buffer@4.0.1: {} - data-urls@4.0.0: dependencies: abab: 2.0.6 @@ -8151,9 +8263,11 @@ snapshots: optionalDependencies: supports-color: 5.5.0 - debug@4.3.4: + debug@4.3.4(supports-color@9.4.0): dependencies: ms: 2.1.2 + optionalDependencies: + supports-color: 9.4.0 decimal.js-light@2.5.1: {} @@ -8400,10 +8514,10 @@ snapshots: eslint-import-resolver-typescript@3.6.1(eslint-plugin-import@2.27.5)(eslint@8.38.0): dependencies: - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) enhanced-resolve: 5.15.1 eslint: 8.38.0 - eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.57.1(eslint@8.38.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.6.1(eslint-plugin-import@2.27.5)(eslint@8.38.0))(eslint@8.38.0) + eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.57.1(eslint@8.38.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.6.1(eslint-plugin-import@2.27.5)(eslint@8.38.0))(eslint@8.38.0) eslint-plugin-import: 2.27.5(eslint-import-resolver-typescript@3.6.1)(eslint@8.38.0) fast-glob: 3.3.2 get-tsconfig: 4.6.2 @@ -8415,11 +8529,11 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.7.4(@typescript-eslint/parser@5.57.1(eslint@8.38.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.6.1(eslint-plugin-import@2.27.5)(eslint@8.38.0))(eslint@8.38.0): + eslint-module-utils@2.7.4(@typescript-eslint/parser@5.57.1(eslint@8.38.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.6.1(eslint-plugin-import@2.27.5)(eslint@8.38.0))(eslint@8.38.0): dependencies: debug: 3.2.7(supports-color@5.5.0) optionalDependencies: - '@typescript-eslint/parser': 5.57.1(eslint@8.38.0)(typescript@4.9.5) + '@typescript-eslint/parser': 5.57.1(eslint@8.38.0)(typescript@5.6.3) eslint: 8.38.0 eslint-import-resolver-node: 0.3.7 eslint-import-resolver-typescript: 3.6.1(eslint-plugin-import@2.27.5)(eslint@8.38.0) @@ -8436,7 +8550,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.57.1(eslint@8.38.0)(typescript@4.9.5))(eslint-import-resolver-typescript@3.6.1(eslint-plugin-import@2.27.5)(eslint@8.38.0))(eslint@8.38.0): + eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.57.1(eslint@8.38.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1(eslint-plugin-import@2.27.5)(eslint@8.38.0))(eslint@8.38.0): dependencies: array-includes: 3.1.6 array.prototype.flat: 1.3.1 @@ -8445,7 +8559,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.38.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.57.1(eslint@8.38.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.6.1(eslint-plugin-import@2.27.5)(eslint@8.38.0))(eslint@8.38.0) + eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.57.1(eslint@8.38.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.6.1(eslint-plugin-import@2.27.5)(eslint@8.38.0))(eslint@8.38.0) has: 1.0.3 is-core-module: 2.11.0 is-glob: 4.0.3 @@ -8455,7 +8569,7 @@ snapshots: semver: 6.3.0 tsconfig-paths: 3.14.1 optionalDependencies: - '@typescript-eslint/parser': 5.57.1(eslint@8.38.0)(typescript@4.9.5) + '@typescript-eslint/parser': 5.57.1(eslint@8.38.0)(typescript@5.6.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -8493,7 +8607,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.38.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.57.1(eslint@8.38.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.6.1(eslint-plugin-import@2.27.5)(eslint@8.38.0))(eslint@8.38.0) + eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.57.1(eslint@8.38.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.6.1(eslint-plugin-import@2.27.5)(eslint@8.38.0))(eslint@8.38.0) has: 1.0.3 is-core-module: 2.11.0 is-glob: 4.0.3 @@ -8574,7 +8688,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.1.1 @@ -8619,7 +8733,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.1.1 @@ -8760,10 +8874,6 @@ snapshots: dependencies: strnum: 1.0.5 - fast-xml-parser@4.2.4: - dependencies: - strnum: 1.0.5 - fast-xml-parser@4.2.5: dependencies: strnum: 1.0.5 @@ -8772,11 +8882,6 @@ snapshots: dependencies: reusify: 1.0.4 - fetch-blob@3.2.0: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.2.1 - file-entry-cache@6.0.1: dependencies: flat-cache: 3.0.4 @@ -8823,10 +8928,6 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 - formdata-polyfill@4.0.10: - dependencies: - fetch-blob: 3.2.0 - forwarded@0.2.0: {} fresh@0.5.2: {} @@ -8965,14 +9066,21 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) transitivePeerDependencies: - supports-color https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.5(supports-color@9.4.0): + dependencies: + agent-base: 7.1.1(supports-color@9.4.0) + debug: 4.3.4(supports-color@9.4.0) transitivePeerDependencies: - supports-color @@ -9012,6 +9120,8 @@ snapshots: indent-string@4.0.0: {} + index-to-position@0.1.2: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -9151,6 +9261,8 @@ snapshots: jmespath@0.16.0: {} + js-levenshtein@1.1.6: {} + js-sdsl@4.3.0: {} js-tokens@4.0.0: {} @@ -9200,6 +9312,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: @@ -9293,7 +9407,7 @@ snapshots: cli-truncate: 3.1.0 colorette: 2.0.19 commander: 9.5.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) execa: 6.1.0 lilconfig: 2.0.6 listr2: 5.0.7 @@ -9326,6 +9440,8 @@ snapshots: lodash-es@4.17.21: {} + lodash.isequal@4.5.0: {} + lodash.merge@4.6.2: {} lodash@4.17.21: {} @@ -9412,6 +9528,10 @@ snapshots: dependencies: brace-expansion: 1.1.11 + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + minimist@1.2.8: {} mlly@1.4.0: @@ -9471,7 +9591,7 @@ snapshots: mquery@5.0.0: dependencies: - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) transitivePeerDependencies: - supports-color @@ -9493,13 +9613,9 @@ snapshots: negotiator@0.6.3: {} - node-domexception@1.0.0: {} - - node-fetch@3.3.1: + node-fetch@2.7.0: dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 + whatwg-url: 5.0.0 node-releases@2.0.10: {} @@ -9600,6 +9716,18 @@ snapshots: dependencies: mimic-fn: 4.0.0 + openapi-typescript@7.4.3(typescript@5.6.3): + dependencies: + '@redocly/openapi-core': 1.25.11(supports-color@9.4.0) + ansi-colors: 4.1.3 + change-case: 5.4.4 + parse-json: 8.1.0 + supports-color: 9.4.0 + typescript: 5.6.3 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - encoding + optionator@0.9.1: dependencies: deep-is: 0.1.4 @@ -9640,6 +9768,12 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-json@8.1.0: + dependencies: + '@babel/code-frame': 7.22.13 + index-to-position: 0.1.2 + type-fest: 4.26.1 + parse5@7.1.2: dependencies: entities: 4.4.0 @@ -9680,6 +9814,8 @@ snapshots: mlly: 1.4.0 pathe: 1.1.1 + pluralize@8.0.0: {} + popper.js@1.16.1-lts: {} postcss-value-parser@3.3.1: {} @@ -9934,6 +10070,8 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + requires-port@1.0.0: {} resize-observer-polyfill@1.5.1: {} @@ -10209,6 +10347,8 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-color@9.4.0: {} + supports-preserve-symlinks-flag@1.0.0: {} svg-parser@2.0.4: {} @@ -10256,6 +10396,8 @@ snapshots: universalify: 0.2.0 url-parse: 1.5.10 + tr46@0.0.3: {} + tr46@3.0.0: dependencies: punycode: 2.3.0 @@ -10279,10 +10421,10 @@ snapshots: tslib@2.5.0: {} - tsutils@3.21.0(typescript@4.9.5): + tsutils@3.21.0(typescript@5.6.3): dependencies: tslib: 1.14.1 - typescript: 4.9.5 + typescript: 5.6.3 tsx@3.12.7: dependencies: @@ -10331,6 +10473,8 @@ snapshots: type-fest@0.21.3: {} + type-fest@4.26.1: {} + type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -10342,9 +10486,7 @@ snapshots: for-each: 0.3.3 is-typed-array: 1.1.10 - typescript@4.9.5: {} - - typescript@5.3.3: {} + typescript@5.6.3: {} ua-parser-js@1.0.37: {} @@ -10391,6 +10533,8 @@ snapshots: escalade: 3.1.1 picocolors: 1.0.0 + uri-js-replace@1.0.1: {} + uri-js@4.4.1: dependencies: punycode: 2.3.0 @@ -10449,7 +10593,7 @@ snapshots: vite-node@0.34.4(@types/node@20.11.6): dependencies: cac: 6.7.14 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) mlly: 1.4.0 pathe: 1.1.1 picocolors: 1.0.0 @@ -10505,7 +10649,7 @@ snapshots: acorn-walk: 8.2.0 cac: 6.7.14 chai: 4.3.7 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.4.0) local-pkg: 0.4.3 magic-string: 0.30.2 pathe: 1.1.1 @@ -10541,15 +10685,10 @@ snapshots: glob-to-regexp: 0.4.1 graceful-fs: 4.2.10 - web-streams-polyfill@3.2.1: {} + webidl-conversions@3.0.1: {} webidl-conversions@7.0.0: {} - websoc-api@3.0.0: - dependencies: - fast-xml-parser: 4.2.4 - node-fetch: 3.3.1 - websoc-fuzzy-search@1.0.1: dependencies: base64-arraybuffer: 1.0.2 @@ -10571,6 +10710,11 @@ snapshots: tr46: 4.1.1 webidl-conversions: 7.0.0 + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which-boxed-primitive@1.0.2: dependencies: is-bigint: 1.0.4 @@ -10646,6 +10790,8 @@ snapshots: yallist@4.0.0: {} + yaml-ast-parser@0.0.43: {} + yaml@1.10.2: {} yaml@2.2.1: {} @@ -10676,6 +10822,8 @@ snapshots: property-expr: 2.0.5 toposort: 2.0.2 + zod@3.23.8: {} + zustand@4.3.3(react@18.2.0): dependencies: use-sync-external-store: 1.2.0(react@18.2.0) From 744a5302b7f6afb19262439ea5b8391683e77519 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Sun, 10 Nov 2024 20:28:36 -0800 Subject: [PATCH 02/34] courses display (i think) --- .../RightPane/SectionTable/CourseInfoBar.tsx | 38 ++++++++----------- apps/backend/package.json | 1 - apps/backend/src/routers/course.ts | 3 +- packages/types/src/index.ts | 1 + pnpm-lock.yaml | 3 -- 5 files changed, 19 insertions(+), 27 deletions(-) diff --git a/apps/antalmanac/src/components/RightPane/SectionTable/CourseInfoBar.tsx b/apps/antalmanac/src/components/RightPane/SectionTable/CourseInfoBar.tsx index bbf30e5ae..dd8fa9f1a 100644 --- a/apps/antalmanac/src/components/RightPane/SectionTable/CourseInfoBar.tsx +++ b/apps/antalmanac/src/components/RightPane/SectionTable/CourseInfoBar.tsx @@ -3,7 +3,7 @@ import { withStyles } from '@material-ui/core/styles'; import { ClassNameMap } from '@material-ui/core/styles/withStyles'; import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined'; import { Skeleton } from '@material-ui/lab'; -import { type RawResponse, type Course, isErrorResponse, type PrerequisiteTree } from 'peterportal-api-next-types'; +import type { PrerequisiteTree } from '@packages/antalmanac-types'; import { useState } from 'react'; import { MOBILE_BREAKPOINT } from '../../../globals'; @@ -11,7 +11,7 @@ import { MOBILE_BREAKPOINT } from '../../../globals'; import PrereqTree from './PrereqTree'; import analyticsEnum, { logAnalytics } from '$lib/analytics'; -import { PETERPORTAL_REST_ENDPOINT } from '$lib/api/endpoints'; +import trpc from '$lib/api/trpc'; const styles = () => ({ rightSpace: { @@ -81,27 +81,21 @@ const CourseInfoBar = (props: CourseInfoBarProps) => { if (courseInfo === null) { try { - const courseId = encodeURIComponent( - `${deptCode.replace(/\s/g, '')}${courseNumber.replace(/\s/g, '')}` - ); - const res: RawResponse = await fetch( - `${PETERPORTAL_REST_ENDPOINT}/courses/${courseId}` - ).then((r) => r.json()); - - if (!isErrorResponse(res)) { - const data = res.payload; - + const res = await trpc.course.get.query({ + id: `${deptCode.replace(/\s/g, '')}${courseNumber.replace(/\s/g, '')}`, + }); + if (res) { setCourseInfo({ - id: data.id, - department: data.department, - courseNumber: data.courseNumber, - title: data.title, - prerequisite_tree: data.prerequisiteTree, - prerequisite_list: data.prerequisiteList, - prerequisite_text: data.prerequisiteText, - prerequisite_for: data.prerequisiteFor, - description: data.description, - ge_list: data.geList.join(', '), + id: res.id, + department: res.department, + courseNumber: res.courseNumber, + title: res.title, + prerequisite_tree: res.prerequisiteTree, + prerequisite_list: res.prerequisites.map((x) => x.id), + prerequisite_text: res.prerequisiteText, + prerequisite_for: res.dependencies.map((x) => x.id), + description: res.description, + ge_list: res.geList.join(', '), }); } else { setCourseInfo(noCourseInfo); diff --git a/apps/backend/package.json b/apps/backend/package.json index 03fba78fe..85c0f03b4 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -10,7 +10,6 @@ "lint": "eslint --fix src" }, "dependencies": { - "@packages/anteater-api-schemas": "workspace:*", "@packages/antalmanac-types": "workspace:*", "@trpc/server": "^10.30.0", "@vendia/serverless-express": "^4.10.1", diff --git a/apps/backend/src/routers/course.ts b/apps/backend/src/routers/course.ts index 673d6fe92..3e326d00b 100644 --- a/apps/backend/src/routers/course.ts +++ b/apps/backend/src/routers/course.ts @@ -1,11 +1,12 @@ import {z} from "zod"; +import type { Course } from '@packages/antalmanac-types'; import {procedure, router} from "../trpc"; const courseRouter = router({ get: procedure .input(z.object({ id: z.string() })) .query(async ({ input }) => { - const res = await fetch(`https://anteaterapi.com/v2/rest/courses/${encodeURIComponent(input.id)}`, { + return await fetch(`https://anteaterapi.com/v2/rest/courses/${encodeURIComponent(input.id)}`, { headers: { ...process.env.ANTEATER_API_KEY && { Authorization: `Bearer ${process.env.ANTEATER_API_KEY}`} } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 0a6ddf805..fb56dcdb6 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -3,3 +3,4 @@ export * from './customevent'; export * from './user'; export * from './legacy'; export * from './websoc'; +export * from '@packages/anteater-api-schemas/src/courses'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd66485d6..c3d803519 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -297,9 +297,6 @@ importers: '@packages/antalmanac-types': specifier: workspace:* version: link:../../packages/types - '@packages/anteater-api-schemas': - specifier: workspace:* - version: link:../../packages/anteater-api-schemas '@trpc/server': specifier: ^10.30.0 version: 10.30.0 From fe7c47942e94e3dcb3f34e98e4f76f1081ebaf59 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Mon, 11 Nov 2024 18:00:02 -0800 Subject: [PATCH 03/34] feat: move websoc logic to backend --- .../antalmanac/src/actions/AppStoreActions.ts | 3 +- apps/antalmanac/src/lib/course_data.types.ts | 2 +- apps/antalmanac/src/lib/websoc.ts | 207 +----------------- apps/backend/src/routers/index.ts | 2 + apps/backend/src/routers/websoc.ts | 120 ++++++++++ packages/anteater-api-schemas/src/websoc.ts | 101 +-------- packages/types/src/index.ts | 1 + 7 files changed, 136 insertions(+), 300 deletions(-) create mode 100644 apps/backend/src/routers/websoc.ts diff --git a/apps/antalmanac/src/actions/AppStoreActions.ts b/apps/antalmanac/src/actions/AppStoreActions.ts index e76271f1e..e98046acf 100644 --- a/apps/antalmanac/src/actions/AppStoreActions.ts +++ b/apps/antalmanac/src/actions/AppStoreActions.ts @@ -1,7 +1,6 @@ -import { RepeatingCustomEvent, ScheduleCourse, ShortCourseSchedule } from '@packages/antalmanac-types'; +import { RepeatingCustomEvent, ScheduleCourse, ShortCourseSchedule, WebsocSection } from '@packages/antalmanac-types'; import { TRPCError } from '@trpc/server'; import { VariantType } from 'notistack'; -import { WebsocSection } from 'peterportal-api-next-types'; import { SnackbarPosition } from '$components/NotificationSnackbar'; import analyticsEnum, { logAnalytics, courseNumAsDecimal } from '$lib/analytics'; diff --git a/apps/antalmanac/src/lib/course_data.types.ts b/apps/antalmanac/src/lib/course_data.types.ts index 619d745d6..849e8082a 100644 --- a/apps/antalmanac/src/lib/course_data.types.ts +++ b/apps/antalmanac/src/lib/course_data.types.ts @@ -1,4 +1,4 @@ -import { WebsocSection } from 'peterportal-api-next-types'; +import { WebsocSection } from '@packages/antalmanac-types'; export interface CourseDetails { deptCode: string; diff --git a/apps/antalmanac/src/lib/websoc.ts b/apps/antalmanac/src/lib/websoc.ts index 60997a81c..411828b28 100644 --- a/apps/antalmanac/src/lib/websoc.ts +++ b/apps/antalmanac/src/lib/websoc.ts @@ -1,213 +1,16 @@ -import type { WebsocAPIResponse, WebsocSectionMeeting } from 'peterportal-api-next-types'; - -import { PETERPORTAL_WEBSOC_ENDPOINT } from './api/endpoints'; -import type { CourseInfo } from './course_data.types'; - -interface CacheEntry extends WebsocAPIResponse { - timestamp: number; -} +import trpc from '$lib/api/trpc'; class _WebSOC { - private cache: { [key: string]: CacheEntry }; - - constructor() { - this.cache = {}; - } - - clearCache() { - Object.keys(this.cache).forEach((key) => delete this.cache[key]); //https://stackoverflow.com/a/19316873/14587004 - } - - // Construct a request to PeterPortal with the params as a query string async query(params: Record) { - // Construct a request to PeterPortal with the params as a query string - const url = new URL(PETERPORTAL_WEBSOC_ENDPOINT); - const searchString = new URLSearchParams(this.cleanSearchParams(params)).toString(); - if (this.cache[searchString]?.timestamp > Date.now() - 30 * 60 * 1000) { - //NOTE: Check out how caching works - //if cache hit and less than 30 minutes old - return this.cache[searchString]; - } - url.search = searchString; - - //The data from the API will duplicate a section if it has multiple locations. - //I.e., if there's a Tuesday section in two different (probably adjoined) rooms, - //courses[i].sections[j].meetings will have two entries, despite it being the same section. - //For now, I'm correcting it with removeDuplicateMeetings, but the API should handle this - - const response: WebsocAPIResponse = await fetch(url, { - headers: { - Referer: 'https://antalmanac.com/', - }, - }) - .then((r) => r.json()) - .then((r) => r.payload); - this.cache[searchString] = { ...response, timestamp: Date.now() }; - return this.removeDuplicateMeetings(response); + return await trpc.websoc.getOne.query(params); } async queryMultiple(params: { [key: string]: string }, fieldName: string) { - const responses: WebsocAPIResponse[] = []; - for (const field of params[fieldName].trim().replace(' ', '').split(',')) { - const req = JSON.parse(JSON.stringify(params)) as Record; - req[fieldName] = field; - responses.push(await this.query(req)); - } - - return this.combineSOCObjects(responses); - } - - async getCourseInfo(websoc_params: Record) { - const SOCObject = await this.query(websoc_params); - - const courseInfo: { [sectionCode: string]: CourseInfo } = {}; - for (const school of SOCObject.schools) { - for (const department of school.departments) { - for (const course of department.courses) { - for (const section of course.sections) { - courseInfo[section.sectionCode] = { - courseDetails: { - deptCode: department.deptCode, - courseNumber: course.courseNumber, - courseTitle: course.courseTitle, - courseComment: course.courseComment, - prerequisiteLink: course.prerequisiteLink, - }, - section: section, - }; - } - } - } - } - return courseInfo; - } - - private combineSOCObjects(SOCObjects: WebsocAPIResponse[]) { - const combined = SOCObjects.shift() as WebsocAPIResponse; - for (const res of SOCObjects) { - for (const school of res.schools) { - const schoolIndex = combined.schools.findIndex((s) => s.schoolName === school.schoolName); - if (schoolIndex !== -1) { - for (const dept of school.departments) { - const deptIndex = combined.schools[schoolIndex].departments.findIndex( - (d) => d.deptCode === dept.deptCode - ); - if (deptIndex !== -1) { - const courses = new Set(combined.schools[schoolIndex].departments[deptIndex].courses); - for (const course of dept.courses) { - courses.add(course); - } - const coursesArray = Array.from(courses); - coursesArray.sort( - (left, right) => - parseInt(left.courseNumber.replace(/\D/g, '')) - - parseInt(right.courseNumber.replace(/\D/g, '')) - ); - combined.schools[schoolIndex].departments[deptIndex].courses = coursesArray; - } else { - combined.schools[schoolIndex].departments.push(dept); - } - } - } else { - combined.schools.push(school); - } - } - } - return combined; + return await trpc.websoc.getMany.query({ params, fieldName }); } - // Removes duplicate meetings as a result of multiple locations from WebsocAPIResponse. - // See queryWebsoc for more info - // NOTE: The separator is currently an ampersand. Maybe it should be refactored to be an array - // TODO: Remove if and when API is fixed - // Maybe put this into CourseRenderPane.tsx -> flattenSOCObject() - private removeDuplicateMeetings(websocResp: WebsocAPIResponse): WebsocAPIResponse { - websocResp.schools.forEach((school, schoolIndex) => { - school.departments.forEach((department, departmentIndex) => { - department.courses.forEach((course, courseIndex) => { - course.sections.forEach((section, sectionIndex) => { - // Merge meetings that have the same meeting day and time - - const existingMeetings: WebsocSectionMeeting[] = []; - - // I know that this is n^2, but a section can't have *that* many locations - for (const meeting of section.meetings) { - let isNewMeeting = true; - - for (let i = 0; i < existingMeetings.length; i++) { - const sameDayAndTime = - meeting.days === existingMeetings[i].days && - meeting.startTime === existingMeetings[i].startTime && - meeting.endTime === existingMeetings[i].endTime; - const sameBuilding = meeting.bldg === existingMeetings[i].bldg; - - //This shouldn't be possible because there shouldn't be duplicate locations in a section - if (sameDayAndTime && sameBuilding) { - console.warn('Found two meetings with same days, time, and bldg', websocResp); - break; - } - - // Add the building to existing meeting instead of creating a new one - if (sameDayAndTime && !sameBuilding) { - existingMeetings[i] = { - timeIsTBA: existingMeetings[i].timeIsTBA, - days: existingMeetings[i].days, - startTime: existingMeetings[i].startTime, - endTime: existingMeetings[i].endTime, - bldg: [existingMeetings[i].bldg + ' & ' + meeting.bldg], - }; - isNewMeeting = false; - } - } - - if (isNewMeeting) existingMeetings.push(meeting); - } - - // Update websocResp with correct meetings - websocResp.schools[schoolIndex].departments[departmentIndex].courses[courseIndex].sections[ - sectionIndex - ].meetings = existingMeetings; - }); - }); - }); - }); - return websocResp; - } - - private cleanSearchParams(record: Record) { - if ('term' in record) { - const termValue = record['term']; - const termParts = termValue.split(' '); - - if (termParts.length === 2) { - const [year, quarter] = termParts; - - delete record['term']; - - record['quarter'] = quarter; - record['year'] = year; - } - } - - if ('startTime' in record) { - if (record['startTime'] === '') { - delete record['startTime']; - } - } - - if ('endTime' in record) { - if (record['endTime'] === '') { - delete record['endTime']; - } - } - - if ('division' in record) { - if (record['division'] === '') { - delete record['division']; - } - } - - return record; + async getCourseInfo(params: Record) { + return await trpc.websoc.getCourseInfo.query(params); } } diff --git a/apps/backend/src/routers/index.ts b/apps/backend/src/routers/index.ts index 3ef1c8b08..8888a6ae3 100644 --- a/apps/backend/src/routers/index.ts +++ b/apps/backend/src/routers/index.ts @@ -3,11 +3,13 @@ import newsRouter from './news'; import usersRouter from './users'; import zotcourseRouter from "./zotcours"; import courseRouter from "./course"; +import websocRouter from "./websoc"; const appRouter = router({ course: courseRouter, news: newsRouter, users: usersRouter, + websoc: websocRouter, zotcourse: zotcourseRouter }); diff --git a/apps/backend/src/routers/websoc.ts b/apps/backend/src/routers/websoc.ts new file mode 100644 index 000000000..163240002 --- /dev/null +++ b/apps/backend/src/routers/websoc.ts @@ -0,0 +1,120 @@ +import {z} from "zod"; +import {procedure, router} from "../trpc"; +import type { WebsocAPIResponse } from '@packages/antalmanac-types'; +import type {CourseInfo} from "$aa/src/lib/course_data.types"; + +function cleanSearchParams(record: Record) { + if ('term' in record) { + const termValue = record['term']; + const termParts = termValue.split(' '); + if (termParts.length === 2) { + const [year, quarter] = termParts; + delete record['term']; + record['quarter'] = quarter; + record['year'] = year; + } + } + if ('startTime' in record) { + if (record['startTime'] === '') { + delete record['startTime']; + } + } + if ('endTime' in record) { + if (record['endTime'] === '') { + delete record['endTime']; + } + } + if ('division' in record) { + if (record['division'] === '') { + delete record['division']; + } + } + return record; +} + +const queryWebSoc = async ({ input }: { input: Record }) => + await fetch(`https://anteaterapi.com/v2/rest/websoc?${new URLSearchParams(cleanSearchParams(input))}`, { + headers: { + ...(process.env.ANTEATER_API_KEY && { Authorization: `Bearer ${process.env.ANTEATER_API_KEY}` }), + }, + }) + .then((data) => data.json()) + .then((data) => data.data as WebsocAPIResponse); + +function combineSOCObjects(SOCObjects: WebsocAPIResponse[]) { + const combined = SOCObjects.shift() as WebsocAPIResponse; + for (const res of SOCObjects) { + for (const school of res.schools) { + const schoolIndex = combined.schools.findIndex((s) => s.schoolName === school.schoolName); + if (schoolIndex !== -1) { + for (const dept of school.departments) { + const deptIndex = combined.schools[schoolIndex].departments.findIndex( + (d) => d.deptCode === dept.deptCode + ); + if (deptIndex !== -1) { + const courses = new Set(combined.schools[schoolIndex].departments[deptIndex].courses); + for (const course of dept.courses) { + courses.add(course); + } + const coursesArray = Array.from(courses); + coursesArray.sort( + (left, right) => + parseInt(left.courseNumber.replace(/\D/g, '')) - + parseInt(right.courseNumber.replace(/\D/g, '')) + ); + combined.schools[schoolIndex].departments[deptIndex].courses = coursesArray; + } else { + combined.schools[schoolIndex].departments.push(dept); + } + } + } else { + combined.schools.push(school); + } + } + } + return combined; +} + +const websocRouter = router({ + getOne: procedure + .input(z.record(z.string(), z.string())) + .query(queryWebSoc), + getMany: procedure + .input(z.object({ params: z.record(z.string(), z.string()), fieldName: z.string() })) + .query(async ({ input }) => { + const responses: WebsocAPIResponse[] = []; + for (const field of input.params[input.fieldName].trim().replace(' ', '').split(',')) { + const req = JSON.parse(JSON.stringify(input.params)) as Record; + req[input.fieldName] = field; + responses.push(await queryWebSoc({ input: req })); + } + return combineSOCObjects(responses); + }) + getCourseInfo: procedure + .input(z.record(z.string(), z.string())) + .query(async ({ input }) => { + const res = await queryWebSoc({ input }); + const courseInfo: { [sectionCode: string]: CourseInfo } = {}; + for (const school of res.schools) { + for (const department of school.departments) { + for (const course of department.courses) { + for (const section of course.sections) { + courseInfo[section.sectionCode] = { + courseDetails: { + deptCode: department.deptCode, + courseNumber: course.courseNumber, + courseTitle: course.courseTitle, + courseComment: course.courseComment, + prerequisiteLink: course.prerequisiteLink, + }, + section: section, + }; + } + } + } + } + return courseInfo; + }) +}) + +export default websocRouter; diff --git a/packages/anteater-api-schemas/src/websoc.ts b/packages/anteater-api-schemas/src/websoc.ts index 3551f7e74..a22757e8b 100644 --- a/packages/anteater-api-schemas/src/websoc.ts +++ b/packages/anteater-api-schemas/src/websoc.ts @@ -1,98 +1,9 @@ -import { type Infer, arrayOf, type, union } from 'arktype'; -import { type Quarter, quarters } from 'peterportal-api-next-types'; -import enumerate from './enumerate'; +import { paths } from './generated/anteater-api-types'; -export const HourMinute = type({ - hour: 'number', - minute: 'number', -}); +export type WebsocAPIResponse = + paths['/v2/rest/websoc']['get']['responses'][200]['content']['application/json']['data']; -export const WebsocSectionMeeting = type({ - timeIsTBA: 'boolean', - bldg: 'string[]', - days: 'string | null', - startTime: union(HourMinute, 'null'), - endTime: union(HourMinute, 'null'), -}); +export type WebsocSection = + WebsocAPIResponse['schools'][number]['departments'][number]['courses'][number]['sections'][number]; -export const WebsocSectionEnrollment = type({ - totalEnrolled: 'string', - sectionEnrolled: 'string', -}); - -export const WebSocSectionFinals = type({ - examStatus: '"NO_FINAL" | "TBA_FINAL" | "SCHEDULED_FINAL"', - dayOfWeek: '"Sun" | "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" | null', - month: 'number | null', - day: 'number | null', - startTime: union(HourMinute, 'null'), - endTime: union(HourMinute, 'null'), - bldg: 'string[] | null', -}); - -export const WebsocSection = type({ - sectionCode: 'string', - sectionType: 'string', - sectionNum: 'string', - units: 'string', - instructors: 'string[]', - meetings: arrayOf(WebsocSectionMeeting), - finalExam: WebSocSectionFinals, - maxCapacity: 'string', - numCurrentlyEnrolled: WebsocSectionEnrollment, - numOnWaitlist: 'string', - numWaitlistCap: 'string', - numRequested: 'string', - numNewOnlyReserved: 'string', - restrictions: 'string', - status: enumerate(['OPEN', 'Waitl', 'FULL', 'NewOnly'] as const), - sectionComment: 'string', -}); - -export const WebsocCourse = type({ - deptCode: 'string', - courseNumber: 'string', - courseTitle: 'string', - courseComment: 'string', - prerequisiteLink: 'string', - // sections: arrayOf(WebsocSection), - // Commenting out sections because I don't know how to override this property -}); - -export const WebsocDepartment = type({ - deptName: 'string', - deptCode: 'string', - deptComment: 'string', - courses: arrayOf(WebsocCourse), - sectionCodeRangeComments: 'string[]', - courseNumberRangeComments: 'string[]', -}); - -export const WebsocSchool = type({ - schoolName: 'string', - schoolComment: 'string', - departments: arrayOf(WebsocDepartment), -}); - -export const Term = type({ - year: 'string', - quarter: enumerate(quarters), -}); - -export const WebsocAPIResponse = { - schools: arrayOf(WebsocSchool), -}; - -export const Department = type({ - deptLabel: 'string', - deptValue: 'string', -}); - -export const DepartmentResponse = arrayOf(Department); - -export const TermData = type({ - shortName: 'string' as Infer<`${string} ${Quarter}`>, - longName: 'string', -}); - -export const TermResponse = arrayOf(TermData); +export type WebsocSectionMeeting = WebsocSection['meetings'][number]; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index fb56dcdb6..9c4420a32 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -4,3 +4,4 @@ export * from './user'; export * from './legacy'; export * from './websoc'; export * from '@packages/anteater-api-schemas/src/courses'; +export * from '@packages/anteater-api-schemas/src/websoc'; From f0a0cccc75e4c0cd422ffea192ade1a4098e11ea Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Mon, 11 Nov 2024 18:02:48 -0800 Subject: [PATCH 04/34] feat: reimplement 'clearCache()' --- apps/antalmanac/src/lib/websoc.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/antalmanac/src/lib/websoc.ts b/apps/antalmanac/src/lib/websoc.ts index 411828b28..ea2217de5 100644 --- a/apps/antalmanac/src/lib/websoc.ts +++ b/apps/antalmanac/src/lib/websoc.ts @@ -1,8 +1,14 @@ import trpc from '$lib/api/trpc'; class _WebSOC { + private aaCacheKey = Date.now().toString(10); + + clearCache() { + this.aaCacheKey = Date.now().toString(10); + } + async query(params: Record) { - return await trpc.websoc.getOne.query(params); + return await trpc.websoc.getOne.query({ ...params, aaCacheKey: this.aaCacheKey }); } async queryMultiple(params: { [key: string]: string }, fieldName: string) { From 80d8c326c942d00fe7666c8c77896db671d5a749 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Mon, 11 Nov 2024 18:15:32 -0800 Subject: [PATCH 05/34] feat: something --- apps/backend/src/routers/websoc.ts | 4 +- packages/anteater-api-schemas/src/grades.ts | 47 +-------------------- 2 files changed, 4 insertions(+), 47 deletions(-) diff --git a/apps/backend/src/routers/websoc.ts b/apps/backend/src/routers/websoc.ts index 163240002..f430912a1 100644 --- a/apps/backend/src/routers/websoc.ts +++ b/apps/backend/src/routers/websoc.ts @@ -1,6 +1,6 @@ import {z} from "zod"; -import {procedure, router} from "../trpc"; import type { WebsocAPIResponse } from '@packages/antalmanac-types'; +import {procedure, router} from "../trpc"; import type {CourseInfo} from "$aa/src/lib/course_data.types"; function cleanSearchParams(record: Record) { @@ -89,7 +89,7 @@ const websocRouter = router({ responses.push(await queryWebSoc({ input: req })); } return combineSOCObjects(responses); - }) + }), getCourseInfo: procedure .input(z.record(z.string(), z.string())) .query(async ({ input }) => { diff --git a/packages/anteater-api-schemas/src/grades.ts b/packages/anteater-api-schemas/src/grades.ts index 3ee994f8b..76c90623d 100644 --- a/packages/anteater-api-schemas/src/grades.ts +++ b/packages/anteater-api-schemas/src/grades.ts @@ -1,46 +1,3 @@ -import { arrayOf, type } from 'arktype'; -import { quarters } from 'peterportal-api-next-types'; -import enumerate from './enumerate'; +import { paths } from './generated/anteater-api-types'; -/** - * A section which has grades data associated with it. - */ -export const GradeSection = type({ - year: 'string', - quarter: enumerate(quarters), - department: 'string', - courseNumber: 'string', - courseNumeric: 'number', - sectionCode: 'string', - instructors: 'string[]', -}); - -export const GradeDistribution = type({ - gradeACount: 'number', - gradeBCount: 'number', - gradeCCount: 'number', - gradeDCount: 'number', - gradeFCount: 'number', - gradePCount: 'number', - gradeNPCount: 'number', - gradeWCount: 'number', - averageGPA: 'number', -}); - -/** - * The type of the payload returned on a successful response from querying - * ``/v1/rest/grades/raw``. - * @alpha - */ -export const GradesRaw = arrayOf(type([GradeSection, '&', GradeDistribution])); - -/** - * An object that represents aggregate grades statistics for a given query. - * The type of the payload returned on a successful response from querying - * ``/v1/rest/grades/aggregate``. - * @alpha - */ -export const GradesAggregate = type({ - sectionList: arrayOf(GradeSection), - gradeDistribution: GradeDistribution, -}); +export type GE = NonNullable['ge']>; From 83728604cc1e5837185bb7884721d776a142a172 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Mon, 11 Nov 2024 18:47:33 -0800 Subject: [PATCH 06/34] feat: grades trpc --- apps/antalmanac/src/lib/grades.ts | 79 ++++----------------- apps/backend/src/routers/grades.ts | 24 +++++++ apps/backend/src/routers/index.ts | 2 + packages/anteater-api-schemas/src/grades.ts | 6 ++ packages/types/src/index.ts | 1 + 5 files changed, 46 insertions(+), 66 deletions(-) create mode 100644 apps/backend/src/routers/grades.ts diff --git a/apps/antalmanac/src/lib/grades.ts b/apps/antalmanac/src/lib/grades.ts index c6a4e06c3..b3bfa0b18 100644 --- a/apps/antalmanac/src/lib/grades.ts +++ b/apps/antalmanac/src/lib/grades.ts @@ -1,9 +1,9 @@ -import { GE } from 'peterportal-api-next-types'; +import { GE } from '@packages/antalmanac-types'; -import { queryGraphQL } from './helpers'; +import trpc from '$lib/api/trpc'; export interface GradesProps { - averageGPA: number; + averageGPA: number | null; gradeACount: number; gradeBCount: number; gradeCCount: number; @@ -13,26 +13,6 @@ export interface GradesProps { gradeNPCount: number; } -export interface CourseInstructorGrades extends GradesProps { - department: string; - courseNumber: string; - instructor: string; -} - -export interface GradesGraphQLResponse { - data: { - aggregateGrades: { - gradeDistribution: GradesProps; - }; - }; -} - -export interface GroupedGradesGraphQLResponse { - data: { - aggregateByOffering: Array; - }; -} - /** * Class to handle querying and caching of grades. * Retrieves grades from Anteater API. @@ -55,7 +35,7 @@ class _Grades { } /* - * Query the PeterPortal GraphQL API (aggregrateGroupedGrades) for the grades of all course-instructor if not already cached. + * Query the Anteater API for the grades of all course-instructor if not already cached. * This should be done before queryGrades to avoid DoS'ing the server * * Either department or ge must be provided @@ -68,32 +48,14 @@ class _Grades { department = department != 'ALL' ? department : undefined; ge = ge != 'ANY' ? ge : undefined; - if (!department && !ge) throw new Error('populategradesCache: Must provide either department or ge'); + if (!department && !ge) throw new Error('populateGradesCache: Must provide either department or ge'); const queryKey = `${department ?? ''}${ge ?? ''}`; // If the whole query has already been cached, return if (this.cachedQueries.has(queryKey)) return; - const filter = `${ge ? `ge: ${ge.replace('-', '_')} ` : ''}${department ? `department: "${department}" ` : ''}`; - - const response = await queryGraphQL(`{ - aggregateByOffering(${filter}) { - department - courseNumber - instructor - averageGPA - gradeACount - gradeBCount - gradeCCount - gradeDCount - gradeFCount - gradeNPCount - gradePCount - } - }`); - - const groupedGrades = response?.data?.aggregateByOffering; + const groupedGrades = await trpc.grades.aggregateByOffering.query({ department, ge }); if (!groupedGrades) throw new Error('populateGradesCache: Failed to query GraphQL'); @@ -116,8 +78,8 @@ class _Grades { }; /* - * Query the PeterPortal GraphQL API for a course's grades with caching - * This should NOT be done individually and independantly to fetch large amounts of data. Use populateGradesCache first to avoid DoS'ing the server + * Query the AnteaterAPI API for a course's grades with caching + * This should NOT be done individually and independently to fetch large amounts of data. Use populateGradesCache first to avoid DoS'ing the server * * @param deptCode The department code of the course. * @param courseNumber The course number of the course. @@ -127,37 +89,22 @@ class _Grades { * @returns Grades */ queryGrades = async ( - deptCode: string, + department: string, courseNumber: string, instructor = '', cacheOnly = true ): Promise => { instructor = instructor.replace('STAFF', '').trim(); // Ignore STAFF - const instructorFilter = instructor ? `instructor: "${instructor}"` : ''; - const cacheKey = deptCode + courseNumber + instructor; + const cacheKey = department + courseNumber + instructor; if (cacheKey in this.gradesCache) return this.gradesCache[cacheKey]; if (cacheOnly) return null; - const queryString = `{ - aggregateGrades(department: "${deptCode}", courseNumber: "${courseNumber}", ${instructorFilter}) { - gradeDistribution { - gradeACount - gradeBCount - gradeCCount - gradeDCount - gradeFCount - gradePCount - gradeNPCount - averageGPA - } - }, - }`; - - const resp = - (await queryGraphQL(queryString))?.data?.aggregateGrades?.gradeDistribution ?? null; + const resp = await trpc.grades.aggregateGrades + .query({ department, courseNumber, instructor }) + .then((x) => x?.gradeDistribution ?? null); if (resp) this.gradesCache[cacheKey] = resp; diff --git a/apps/backend/src/routers/grades.ts b/apps/backend/src/routers/grades.ts new file mode 100644 index 000000000..0c64524ef --- /dev/null +++ b/apps/backend/src/routers/grades.ts @@ -0,0 +1,24 @@ +import type {AggregateGrades, AggregateGradesByOffering} from '@packages/antalmanac-types'; +import {z} from "zod"; +import {procedure, router} from "../trpc"; + +const gradesRouter = router({ + aggregateGrades: procedure + .input(z.object({ + department: z.string().optional(), + courseNumber: z.string().optional(), + instructor: z.string().optional(), + ge: z.string().optional() + })) + .query(async ({ input }) => await fetch(`https://anteaterapi.com/v2/rest/grades/aggregate?${new URLSearchParams(input)}`).then(x => x.json()).then(x => x.data as AggregateGrades)), + aggregateByOffering: procedure + .input(z.object({ + department: z.string().optional(), + courseNumber: z.string().optional(), + instructor: z.string().optional(), + ge: z.string().optional() + })) + .query(async ({ input }) => await fetch(`https://anteaterapi.com/v2/rest/grades/aggregateByOffering?${new URLSearchParams(input)}`).then(x => x.json()).then(x => x.data as AggregateGradesByOffering)) +}); + +export default gradesRouter; diff --git a/apps/backend/src/routers/index.ts b/apps/backend/src/routers/index.ts index 8888a6ae3..e82ec38dd 100644 --- a/apps/backend/src/routers/index.ts +++ b/apps/backend/src/routers/index.ts @@ -4,9 +4,11 @@ import usersRouter from './users'; import zotcourseRouter from "./zotcours"; import courseRouter from "./course"; import websocRouter from "./websoc"; +import gradesRouter from "./grades"; const appRouter = router({ course: courseRouter, + grades: gradesRouter, news: newsRouter, users: usersRouter, websoc: websocRouter, diff --git a/packages/anteater-api-schemas/src/grades.ts b/packages/anteater-api-schemas/src/grades.ts index 76c90623d..1ef88992f 100644 --- a/packages/anteater-api-schemas/src/grades.ts +++ b/packages/anteater-api-schemas/src/grades.ts @@ -1,3 +1,9 @@ import { paths } from './generated/anteater-api-types'; export type GE = NonNullable['ge']>; + +export type AggregateGrades = + paths['/v2/rest/grades/aggregate']['get']['responses']['200']['content']['application/json']['data']; + +export type AggregateGradesByOffering = + paths['/v2/rest/grades/aggregateByOffering']['get']['responses']['200']['content']['application/json']['data']; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 9c4420a32..cfc71b429 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -4,4 +4,5 @@ export * from './user'; export * from './legacy'; export * from './websoc'; export * from '@packages/anteater-api-schemas/src/courses'; +export * from '@packages/anteater-api-schemas/src/grades'; export * from '@packages/anteater-api-schemas/src/websoc'; From 5ea8ba6cef015f05d45d237511b53f83f6a0ff06 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Mon, 11 Nov 2024 18:53:42 -0800 Subject: [PATCH 07/34] feat: enrollhist trpc --- apps/antalmanac/src/lib/enrollmentHistory.ts | 23 +++---------------- apps/backend/src/routers/enrollHist.ts | 11 +++++++++ apps/backend/src/routers/index.ts | 2 ++ .../anteater-api-schemas/src/enrollHist.ts | 4 ++++ .../anteater-api-schemas/src/enumerate.ts | 5 ---- packages/anteater-api-schemas/src/index.ts | 1 + packages/types/src/index.ts | 1 + 7 files changed, 22 insertions(+), 25 deletions(-) create mode 100644 apps/backend/src/routers/enrollHist.ts create mode 100644 packages/anteater-api-schemas/src/enrollHist.ts delete mode 100644 packages/anteater-api-schemas/src/enumerate.ts diff --git a/apps/antalmanac/src/lib/enrollmentHistory.ts b/apps/antalmanac/src/lib/enrollmentHistory.ts index d45ec24fd..54c6e7f5c 100644 --- a/apps/antalmanac/src/lib/enrollmentHistory.ts +++ b/apps/antalmanac/src/lib/enrollmentHistory.ts @@ -1,6 +1,7 @@ -import { queryGraphQL } from './helpers'; import { termData } from './termData'; +import trpc from '$lib/api/trpc'; + // This represents the enrollment history of a course section during one quarter export interface EnrollmentHistoryGraphQL { year: string; @@ -46,26 +47,11 @@ export class DepartmentEnrollmentHistory { // Each key in the cache will be the department and courseNumber concatenated static enrollmentHistoryCache: Record = {}; static termShortNames: string[] = termData.map((term) => term.shortName); - static QUERY_TEMPLATE = `{ - enrollmentHistory(department: "$$DEPARTMENT$$", courseNumber: "$$COURSE_NUMBER$$", sectionType: Lec) { - year - quarter - department - courseNumber - dates - totalEnrolledHistory - maxCapacityHistory - waitlistHistory - instructors - } - }`; department: string; - partialQueryString: string; constructor(department: string) { this.department = department; - this.partialQueryString = DepartmentEnrollmentHistory.QUERY_TEMPLATE.replace('$$DEPARTMENT$$', department); } async find(courseNumber: string): Promise { @@ -75,10 +61,7 @@ export class DepartmentEnrollmentHistory { } async queryEnrollmentHistory(courseNumber: string): Promise { - // Query for the enrollment history of all lecture sections that were offered - const queryString = this.partialQueryString.replace('$$COURSE_NUMBER$$', courseNumber); - - const res = (await queryGraphQL(queryString))?.data?.enrollmentHistory; + const res = await trpc.enrollHist.get.query({ department: this.department, courseNumber, sectionType: 'Lec' }); if (!res?.length) { return null; diff --git a/apps/backend/src/routers/enrollHist.ts b/apps/backend/src/routers/enrollHist.ts new file mode 100644 index 000000000..92fbbfda1 --- /dev/null +++ b/apps/backend/src/routers/enrollHist.ts @@ -0,0 +1,11 @@ +import { EnrollmentHistory } from '@packages/antalmanac-types'; +import {z} from "zod"; +import {procedure, router} from "../trpc"; + +const enrollHistRouter = router({ + get: procedure + .input(z.object({ department: z.string(), courseNumber: z.string(), sectionType: z.string() })) + .query(async ({ input }) => await fetch(`https://anteaterapi.com/v2/rest/enrollmentHistory?${new URLSearchParams(input)}`).then(x => x.json()).then(x => x.data as EnrollmentHistory)) +}); + +export default enrollHistRouter; diff --git a/apps/backend/src/routers/index.ts b/apps/backend/src/routers/index.ts index e82ec38dd..c0a47df21 100644 --- a/apps/backend/src/routers/index.ts +++ b/apps/backend/src/routers/index.ts @@ -5,9 +5,11 @@ import zotcourseRouter from "./zotcours"; import courseRouter from "./course"; import websocRouter from "./websoc"; import gradesRouter from "./grades"; +import enrollHistRouter from "./enrollHist"; const appRouter = router({ course: courseRouter, + enrollHist: enrollHistRouter, grades: gradesRouter, news: newsRouter, users: usersRouter, diff --git a/packages/anteater-api-schemas/src/enrollHist.ts b/packages/anteater-api-schemas/src/enrollHist.ts new file mode 100644 index 000000000..e570991d5 --- /dev/null +++ b/packages/anteater-api-schemas/src/enrollHist.ts @@ -0,0 +1,4 @@ +import { paths } from './generated/anteater-api-types'; + +export type EnrollmentHistory = + paths['/v2/rest/enrollmentHistory']['get']['responses'][200]['content']['application/json']['data']; diff --git a/packages/anteater-api-schemas/src/enumerate.ts b/packages/anteater-api-schemas/src/enumerate.ts deleted file mode 100644 index 5bb1a0e80..000000000 --- a/packages/anteater-api-schemas/src/enumerate.ts +++ /dev/null @@ -1,5 +0,0 @@ -function enumerate(values: T) { - return values.map((v) => `"${v}"`).join('|') as `"${T[number]}"`; -} - -export default enumerate; diff --git a/packages/anteater-api-schemas/src/index.ts b/packages/anteater-api-schemas/src/index.ts index 8f22c1c4b..fd964123c 100644 --- a/packages/anteater-api-schemas/src/index.ts +++ b/packages/anteater-api-schemas/src/index.ts @@ -1,5 +1,6 @@ export * from './calendar'; export * from './courses'; +export * from './enrollHist'; export * from './grades'; export * from './instructor'; export * from './websoc'; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index cfc71b429..f791f0734 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -4,5 +4,6 @@ export * from './user'; export * from './legacy'; export * from './websoc'; export * from '@packages/anteater-api-schemas/src/courses'; +export * from '@packages/anteater-api-schemas/src/enrollHist'; export * from '@packages/anteater-api-schemas/src/grades'; export * from '@packages/anteater-api-schemas/src/websoc'; From 5e68bc22ee3daa409f9e8e4ff199ec40b5b359fb Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Mon, 11 Nov 2024 19:06:19 -0800 Subject: [PATCH 08/34] feat: nuke ppapi-next-types --- apps/antalmanac/package.json | 1 - .../RightPane/CoursePane/CourseRenderPane.tsx | 3 +-- .../RightPane/SectionTable/PrereqTree.tsx | 2 +- .../SectionTable/SectionTableBody.tsx | 3 +-- apps/antalmanac/src/lib/api/endpoints.ts | 6 ----- apps/antalmanac/src/lib/download.ts | 3 +-- apps/antalmanac/src/lib/helpers.ts | 23 ------------------ .../src/lib/tourExampleGeneration.ts | 12 ++++------ .../src/stores/calendarizeHelpers.ts | 3 +-- packages/anteater-api-schemas/src/calendar.ts | 15 ------------ .../anteater-api-schemas/src/instructor.ts | 20 ---------------- packages/anteater-api-schemas/src/websoc.ts | 9 ------- .../.eslintrc.yml | 0 .../.gitignore | 0 .../package.json | 7 +++--- .../src/courses.ts | 0 .../src/enrollHist.ts | 0 .../src/grades.ts | 0 .../src/index.ts | 2 -- packages/anteater-api-types/src/websoc.ts | 14 +++++++++++ .../tsconfig.json | 0 packages/types/package.json | 2 +- packages/types/src/index.ts | 8 +++---- packages/types/src/websoc.ts | 24 +++++++------------ pnpm-lock.yaml | 22 +++-------------- 25 files changed, 43 insertions(+), 136 deletions(-) delete mode 100644 packages/anteater-api-schemas/src/calendar.ts delete mode 100644 packages/anteater-api-schemas/src/instructor.ts delete mode 100644 packages/anteater-api-schemas/src/websoc.ts rename packages/{anteater-api-schemas => anteater-api-types}/.eslintrc.yml (100%) rename packages/{anteater-api-schemas => anteater-api-types}/.gitignore (100%) rename packages/{anteater-api-schemas => anteater-api-types}/package.json (64%) rename packages/{anteater-api-schemas => anteater-api-types}/src/courses.ts (100%) rename packages/{anteater-api-schemas => anteater-api-types}/src/enrollHist.ts (100%) rename packages/{anteater-api-schemas => anteater-api-types}/src/grades.ts (100%) rename packages/{anteater-api-schemas => anteater-api-types}/src/index.ts (65%) create mode 100644 packages/anteater-api-types/src/websoc.ts rename packages/{anteater-api-schemas => anteater-api-types}/tsconfig.json (100%) diff --git a/apps/antalmanac/package.json b/apps/antalmanac/package.json index 09cc8b383..e6be52aae 100644 --- a/apps/antalmanac/package.json +++ b/apps/antalmanac/package.json @@ -96,7 +96,6 @@ "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", - "peterportal-api-next-types": "1.0.0-rc.2.68.0", "prettier": "^2.8.4", "typescript": "5.6.3", "vite": "^4.4.9", diff --git a/apps/antalmanac/src/components/RightPane/CoursePane/CourseRenderPane.tsx b/apps/antalmanac/src/components/RightPane/CoursePane/CourseRenderPane.tsx index 2b2feb0d8..8d8726f9b 100644 --- a/apps/antalmanac/src/components/RightPane/CoursePane/CourseRenderPane.tsx +++ b/apps/antalmanac/src/components/RightPane/CoursePane/CourseRenderPane.tsx @@ -1,7 +1,6 @@ import { Close } from '@mui/icons-material'; import { Alert, Box, IconButton, useMediaQuery } from '@mui/material'; -import { AACourse, AASection } from '@packages/antalmanac-types'; -import { WebsocDepartment, WebsocSchool, WebsocAPIResponse, GE } from 'peterportal-api-next-types'; +import { AACourse, AASection, WebsocDepartment, WebsocSchool, WebsocAPIResponse, GE } from '@packages/antalmanac-types'; import { useCallback, useEffect, useState } from 'react'; import LazyLoad from 'react-lazyload'; diff --git a/apps/antalmanac/src/components/RightPane/SectionTable/PrereqTree.tsx b/apps/antalmanac/src/components/RightPane/SectionTable/PrereqTree.tsx index 21ef23214..9e914f3c7 100644 --- a/apps/antalmanac/src/components/RightPane/SectionTable/PrereqTree.tsx +++ b/apps/antalmanac/src/components/RightPane/SectionTable/PrereqTree.tsx @@ -1,6 +1,6 @@ /* eslint-disable prefer-const */ import { Button, Popover } from '@material-ui/core'; -import { Prerequisite, PrerequisiteTree } from 'peterportal-api-next-types'; +import { Prerequisite, PrerequisiteTree } from '@packages/antalmanac-types'; import { FC, useState } from 'react'; import { CourseInfo } from './CourseInfoBar'; diff --git a/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx index 01874a4d4..7b09a36b8 100644 --- a/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx +++ b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx @@ -12,9 +12,8 @@ import { } from '@material-ui/core'; import { withStyles } from '@material-ui/core/styles'; import { ClassNameMap, Styles } from '@material-ui/core/styles/withStyles'; -import { AASection } from '@packages/antalmanac-types'; +import { AASection, WebsocSectionEnrollment, WebsocSectionMeeting } from '@packages/antalmanac-types'; import classNames from 'classnames'; -import { WebsocSectionEnrollment, WebsocSectionMeeting } from 'peterportal-api-next-types'; import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; diff --git a/apps/antalmanac/src/lib/api/endpoints.ts b/apps/antalmanac/src/lib/api/endpoints.ts index 2b8e9eb2f..27fbff1ed 100644 --- a/apps/antalmanac/src/lib/api/endpoints.ts +++ b/apps/antalmanac/src/lib/api/endpoints.ts @@ -15,9 +15,3 @@ export const LOOKUP_NOTIFICATIONS_ENDPOINT = endpointTransform('/api/notificatio export const REGISTER_NOTIFICATIONS_ENDPOINT = endpointTransform('/api/notifications/registerNotifications'); export const MAPBOX_PROXY_DIRECTIONS_ENDPOINT = endpointTransform('/mapbox/directions'); export const TILES_URL = import.meta.env.VITE_TILES_ENDPOINT || 'tile.openstreetmap.org'; - -// PeterPortal API -export const PETERPORTAL_GRAPHQL_ENDPOINT = 'https://api-next.peterportal.org/v1/graphql'; -export const PETERPORTAL_REST_ENDPOINT = 'https://api-next.peterportal.org/v1/rest'; - -export const PETERPORTAL_WEBSOC_ENDPOINT = `${PETERPORTAL_REST_ENDPOINT}/websoc`; diff --git a/apps/antalmanac/src/lib/download.ts b/apps/antalmanac/src/lib/download.ts index 4be092a94..05488c962 100644 --- a/apps/antalmanac/src/lib/download.ts +++ b/apps/antalmanac/src/lib/download.ts @@ -1,8 +1,7 @@ +import type { HourMinute } from '@packages/antalmanac-types'; import { saveAs } from 'file-saver'; import { createEvents, type EventAttributes } from 'ics'; -import type { HourMinute } from 'peterportal-api-next-types'; -import buildingCatalogue from './buildingCatalogue'; import { notNull } from './utils'; import { openSnackbar } from '$actions/AppStoreActions'; diff --git a/apps/antalmanac/src/lib/helpers.ts b/apps/antalmanac/src/lib/helpers.ts index a0a52fdba..43f4645c0 100644 --- a/apps/antalmanac/src/lib/helpers.ts +++ b/apps/antalmanac/src/lib/helpers.ts @@ -1,30 +1,7 @@ import { MouseEvent } from 'react'; -import { PETERPORTAL_GRAPHQL_ENDPOINT } from './api/endpoints'; - import { openSnackbar } from '$actions/AppStoreActions'; -export async function queryGraphQL(queryString: string): Promise { - const query = JSON.stringify({ - query: queryString, - }); - - const res = await fetch(`${PETERPORTAL_GRAPHQL_ENDPOINT}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: query, - }); - - const json = await res.json(); - - if (!res.ok || json.data === null) return null; - - return json as Promise; -} - export const warnMultipleTerms = (terms: Set) => { openSnackbar( 'warning', diff --git a/apps/antalmanac/src/lib/tourExampleGeneration.ts b/apps/antalmanac/src/lib/tourExampleGeneration.ts index f7f937460..bed345940 100644 --- a/apps/antalmanac/src/lib/tourExampleGeneration.ts +++ b/apps/antalmanac/src/lib/tourExampleGeneration.ts @@ -1,17 +1,13 @@ -import { ScheduleCourse } from '@packages/antalmanac-types'; -import { - DayOfWeek, - HourMinute, - WebsocSectionFinalExam, - WebsocSectionMeeting, - daysOfWeek, -} from 'peterportal-api-next-types'; +import { ScheduleCourse, HourMinute, WebsocSectionFinalExam, WebsocSectionMeeting } from '@packages/antalmanac-types'; +import { daysOfWeek } from '$lib/download'; import AppStore from '$stores/AppStore'; const CURRENT_TERM = '2024 Winter'; // TODO: Check the current term when that PR's in let sampleClassesSectionCodes: Array = []; +type DayOfWeek = (typeof daysOfWeek)[number]; + export function addSampleClasses() { if (AppStore.getAddedCourses().length > 0) return; diff --git a/apps/antalmanac/src/stores/calendarizeHelpers.ts b/apps/antalmanac/src/stores/calendarizeHelpers.ts index 8bea13c15..75c5eef44 100644 --- a/apps/antalmanac/src/stores/calendarizeHelpers.ts +++ b/apps/antalmanac/src/stores/calendarizeHelpers.ts @@ -1,5 +1,4 @@ -import type { ScheduleCourse, RepeatingCustomEvent } from '@packages/antalmanac-types'; -import { HourMinute } from 'peterportal-api-next-types'; +import type { ScheduleCourse, RepeatingCustomEvent, HourMinute } from '@packages/antalmanac-types'; import { CourseEvent, CustomEvent, Location } from '$components/Calendar/CourseCalendarEvent'; import { getFinalsStartForTerm } from '$lib/termData'; diff --git a/packages/anteater-api-schemas/src/calendar.ts b/packages/anteater-api-schemas/src/calendar.ts deleted file mode 100644 index 093a591d8..000000000 --- a/packages/anteater-api-schemas/src/calendar.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { type Infer, type } from 'arktype'; -import type { Quarter } from 'peterportal-api-next-types'; - -export const WeekData = type({ - week: 'string', - quarter: 'string' as Infer<`${string} ${Quarter}`>, - display: 'string', -}); - -export const QuarterDates = type({ - instructionStart: 'Date', - instructionEnd: 'Date', - finalsStart: 'Date', - finalsEnd: 'Date', -}); diff --git a/packages/anteater-api-schemas/src/instructor.ts b/packages/anteater-api-schemas/src/instructor.ts deleted file mode 100644 index 07ffb2561..000000000 --- a/packages/anteater-api-schemas/src/instructor.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { arrayOf, type } from 'arktype'; - -/** - * An object representing an instructor. - * The type of the payload returned on a successful response from querying - * ``/v1/rest/instructors/{ucinetid}``. - * @alpha - */ -export const Instructor = type({ - ucinetid: 'string', - instructorName: 'string', - shortenedName: 'string', - title: 'string', - department: 'string', - schools: 'string[]', - relatedDepartments: 'string[]', - courseHistory: 'string[]', -}); - -export const Instructors = arrayOf(Instructor); diff --git a/packages/anteater-api-schemas/src/websoc.ts b/packages/anteater-api-schemas/src/websoc.ts deleted file mode 100644 index a22757e8b..000000000 --- a/packages/anteater-api-schemas/src/websoc.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { paths } from './generated/anteater-api-types'; - -export type WebsocAPIResponse = - paths['/v2/rest/websoc']['get']['responses'][200]['content']['application/json']['data']; - -export type WebsocSection = - WebsocAPIResponse['schools'][number]['departments'][number]['courses'][number]['sections'][number]; - -export type WebsocSectionMeeting = WebsocSection['meetings'][number]; diff --git a/packages/anteater-api-schemas/.eslintrc.yml b/packages/anteater-api-types/.eslintrc.yml similarity index 100% rename from packages/anteater-api-schemas/.eslintrc.yml rename to packages/anteater-api-types/.eslintrc.yml diff --git a/packages/anteater-api-schemas/.gitignore b/packages/anteater-api-types/.gitignore similarity index 100% rename from packages/anteater-api-schemas/.gitignore rename to packages/anteater-api-types/.gitignore diff --git a/packages/anteater-api-schemas/package.json b/packages/anteater-api-types/package.json similarity index 64% rename from packages/anteater-api-schemas/package.json rename to packages/anteater-api-types/package.json index a4d27adab..0d07c492f 100644 --- a/packages/anteater-api-schemas/package.json +++ b/packages/anteater-api-types/package.json @@ -1,7 +1,7 @@ { - "name": "@packages/anteater-api-schemas", + "name": "@packages/anteater-api-types", "version": "0.0.1", - "description": "Internal Anteater API ArkType schemas for AntAlmanac", + "description": "Anteater API types for AntAlmanac", "main": "./src/index.ts", "types": "./src/index.ts", "type": "module", @@ -9,8 +9,7 @@ "postinstall": "openapi-typescript https://anteaterapi.com/openapi.json -o src/generated/anteater-api-types.ts" }, "dependencies": { - "arktype": "1.0.14-alpha", - "peterportal-api-next-types": "1.0.0-alpha.6" + "arktype": "1.0.14-alpha" }, "devDependencies": { "typescript": "5.6.3", diff --git a/packages/anteater-api-schemas/src/courses.ts b/packages/anteater-api-types/src/courses.ts similarity index 100% rename from packages/anteater-api-schemas/src/courses.ts rename to packages/anteater-api-types/src/courses.ts diff --git a/packages/anteater-api-schemas/src/enrollHist.ts b/packages/anteater-api-types/src/enrollHist.ts similarity index 100% rename from packages/anteater-api-schemas/src/enrollHist.ts rename to packages/anteater-api-types/src/enrollHist.ts diff --git a/packages/anteater-api-schemas/src/grades.ts b/packages/anteater-api-types/src/grades.ts similarity index 100% rename from packages/anteater-api-schemas/src/grades.ts rename to packages/anteater-api-types/src/grades.ts diff --git a/packages/anteater-api-schemas/src/index.ts b/packages/anteater-api-types/src/index.ts similarity index 65% rename from packages/anteater-api-schemas/src/index.ts rename to packages/anteater-api-types/src/index.ts index fd964123c..0e0f36f20 100644 --- a/packages/anteater-api-schemas/src/index.ts +++ b/packages/anteater-api-types/src/index.ts @@ -1,6 +1,4 @@ -export * from './calendar'; export * from './courses'; export * from './enrollHist'; export * from './grades'; -export * from './instructor'; export * from './websoc'; diff --git a/packages/anteater-api-types/src/websoc.ts b/packages/anteater-api-types/src/websoc.ts new file mode 100644 index 000000000..68344e66d --- /dev/null +++ b/packages/anteater-api-types/src/websoc.ts @@ -0,0 +1,14 @@ +import { paths } from './generated/anteater-api-types'; + +export type WebsocAPIResponse = + paths['/v2/rest/websoc']['get']['responses'][200]['content']['application/json']['data']; + +export type WebsocCourse = WebsocAPIResponse['schools'][number]['departments'][number]['courses'][number]; + +export type WebsocSection = WebsocCourse['sections'][number]; + +export type WebsocSectionMeeting = WebsocSection['meetings'][number]; + +export type WebsocSectionFinalExam = WebsocSection['finalExam']; + +export type HourMinute = Extract['startTime']; diff --git a/packages/anteater-api-schemas/tsconfig.json b/packages/anteater-api-types/tsconfig.json similarity index 100% rename from packages/anteater-api-schemas/tsconfig.json rename to packages/anteater-api-types/tsconfig.json diff --git a/packages/types/package.json b/packages/types/package.json index dcab957a5..226827237 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "arktype": "1.0.14-alpha", - "@packages/anteater-api-schemas": "workspace:*" + "@packages/anteater-api-types": "workspace:*" }, "devDependencies": { "typescript": "5.6.3" diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index f791f0734..3576e8384 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -3,7 +3,7 @@ export * from './customevent'; export * from './user'; export * from './legacy'; export * from './websoc'; -export * from '@packages/anteater-api-schemas/src/courses'; -export * from '@packages/anteater-api-schemas/src/enrollHist'; -export * from '@packages/anteater-api-schemas/src/grades'; -export * from '@packages/anteater-api-schemas/src/websoc'; +export * from '@packages/anteater-api-types/src/courses'; +export * from '@packages/anteater-api-types/src/enrollHist'; +export * from '@packages/anteater-api-types/src/grades'; +export * from '@packages/anteater-api-types/src/websoc'; diff --git a/packages/types/src/websoc.ts b/packages/types/src/websoc.ts index c1ca181c4..8b7a60060 100644 --- a/packages/types/src/websoc.ts +++ b/packages/types/src/websoc.ts @@ -1,19 +1,13 @@ -import { arrayOf, type } from 'arktype'; -import { - WebsocSection as WebsocSectionSchema, - WebsocCourse as WebsocCourseSchema, -} from '@packages/anteater-api-schemas'; +import { WebsocSection, WebsocCourse } from '@packages/anteater-api-types'; -const AASectionExtendedProperties = type({ - color: 'string', -}); +type AASectionExtendedProperties = { + color: 'string'; +}; -export const AASectionSchema = type([WebsocSectionSchema, '&', AASectionExtendedProperties]); -export type AASection = typeof AASectionSchema.infer; +export type AASection = WebsocSection & AASectionExtendedProperties; -const AACourseExtendedProperties = type({ - sections: arrayOf(AASectionSchema), -}); +type AACourseExtendedProperties = { + sections: AASection[]; +}; -export const AACourseSchema = type([WebsocCourseSchema, '&', AACourseExtendedProperties]); -export type AACourse = typeof AACourseSchema.infer; +export type AACourse = WebsocCourse & AACourseExtendedProperties; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3d803519..36eb8d70f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -276,9 +276,6 @@ importers: eslint-plugin-react-hooks: specifier: ^4.6.0 version: 4.6.0(eslint@8.37.0) - peterportal-api-next-types: - specifier: 1.0.0-rc.2.68.0 - version: 1.0.0-rc.2.68.0 prettier: specifier: ^2.8.4 version: 2.8.4 @@ -423,14 +420,11 @@ importers: specifier: 5.6.3 version: 5.6.3 - packages/anteater-api-schemas: + packages/anteater-api-types: dependencies: arktype: specifier: 1.0.14-alpha version: 1.0.14-alpha - peterportal-api-next-types: - specifier: 1.0.0-alpha.6 - version: 1.0.0-alpha.6 devDependencies: openapi-typescript: specifier: 7.4.3 @@ -441,9 +435,9 @@ importers: packages/types: dependencies: - '@packages/anteater-api-schemas': + '@packages/anteater-api-types': specifier: workspace:* - version: link:../anteater-api-schemas + version: link:../anteater-api-types arktype: specifier: 1.0.14-alpha version: 1.0.14-alpha @@ -4107,12 +4101,6 @@ packages: pathval@1.1.1: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} - peterportal-api-next-types@1.0.0-alpha.6: - resolution: {integrity: sha512-sbQmYiH21t6wIsgFXStJcBZWhMOCjKQspLGdpUEmpYQaR4tL1kwGQ+KNix5EwLJxHM9BnMtK1BJcwu6fOTeqMQ==} - - peterportal-api-next-types@1.0.0-rc.2.68.0: - resolution: {integrity: sha512-gq0k53abt6ea9roA+GlSgP3Rbv+0tC4rGw4gGbrahh+ZNnmTGdlZSF8ISq07DbQ7td8dBev4gMrjrZq+Xn500A==} - picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -9795,10 +9783,6 @@ snapshots: pathval@1.1.1: {} - peterportal-api-next-types@1.0.0-alpha.6: {} - - peterportal-api-next-types@1.0.0-rc.2.68.0: {} - picocolors@1.0.0: {} picomatch@2.3.1: {} From 58482b16f328ac63c90d3070a33b32b16602c461 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Mon, 11 Nov 2024 19:28:50 -0800 Subject: [PATCH 09/34] feat: misc random stuff --- .../SectionTable/SectionTableBody.tsx | 12 ++-- apps/antalmanac/src/lib/grades.ts | 4 +- apps/backend/README.md | 34 ++++----- apps/backend/scripts/build.mjs | 22 +++--- apps/backend/src/db/ddb.ts | 1 - apps/backend/src/index.ts | 10 +-- apps/backend/src/lambda.ts | 2 +- apps/backend/src/routers/course.ts | 20 +++--- apps/backend/src/routers/enrollHist.ts | 17 +++-- apps/backend/src/routers/grades.ts | 63 +++++++++++----- apps/backend/src/routers/index.ts | 12 ++-- apps/backend/src/routers/users.ts | 2 +- apps/backend/src/routers/websoc.ts | 71 ++++++++----------- apps/backend/src/routers/zotcours.ts | 18 +++-- packages/types/src/schedule.ts | 6 +- 15 files changed, 161 insertions(+), 133 deletions(-) diff --git a/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx index 7b09a36b8..a840f9b38 100644 --- a/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx +++ b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx @@ -12,7 +12,7 @@ import { } from '@material-ui/core'; import { withStyles } from '@material-ui/core/styles'; import { ClassNameMap, Styles } from '@material-ui/core/styles/withStyles'; -import { AASection, WebsocSectionEnrollment, WebsocSectionMeeting } from '@packages/antalmanac-types'; +import { AASection, EnrollmentHistory, WebsocSectionMeeting } from '@packages/antalmanac-types'; import classNames from 'classnames'; import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; @@ -315,7 +315,7 @@ const LocationsCell = withStyles(styles)((props: LocationsCellProps) => { return ( {meetings.map((meeting) => { - return meeting.bldg[0] !== 'TBA' ? ( + return !meeting.timeIsTBA ? ( meeting.bldg.map((bldg) => { const [buildingName = ''] = bldg.split(' '); const buildingId = locationIds[buildingName]; @@ -334,7 +334,7 @@ const LocationsCell = withStyles(styles)((props: LocationsCellProps) => { ); }) ) : ( - {meeting.bldg} + {'TBA'} ); })} @@ -343,7 +343,7 @@ const LocationsCell = withStyles(styles)((props: LocationsCellProps) => { interface SectionEnrollmentCellProps { classes: ClassNameMap; - numCurrentlyEnrolled: WebsocSectionEnrollment; + numCurrentlyEnrolled: EnrollmentHistory; maxCapacity: number; /** @@ -430,7 +430,7 @@ const DayAndTimeCell = withStyles(styles)((props: DayAndTimeCellProps) => { {meetings.map((meeting) => { if (meeting.timeIsTBA) { - return TBA; + return TBA; } if (meeting.startTime && meeting.endTime) { @@ -513,7 +513,7 @@ const SectionTableBody = withStyles(styles)((props: SectionTableBodyProps) => { */ const sectionDetails = useMemo(() => { return { - daysOccurring: parseDaysString(section.meetings[0].days), + daysOccurring: parseDaysString(section.meetings[0].timeIsTBA ? null : section.meetings[0].days), ...normalizeTime(section.meetings[0]), }; }, [section.meetings]); diff --git a/apps/antalmanac/src/lib/grades.ts b/apps/antalmanac/src/lib/grades.ts index b3bfa0b18..f5fb91c1e 100644 --- a/apps/antalmanac/src/lib/grades.ts +++ b/apps/antalmanac/src/lib/grades.ts @@ -57,7 +57,9 @@ class _Grades { const groupedGrades = await trpc.grades.aggregateByOffering.query({ department, ge }); - if (!groupedGrades) throw new Error('populateGradesCache: Failed to query GraphQL'); + console.log(groupedGrades); + + if (!groupedGrades) throw new Error('populateGradesCache: Failed to query grades'); // Populate cache for (const course of groupedGrades) { diff --git a/apps/backend/README.md b/apps/backend/README.md index 336ee6a70..2bccdf26a 100644 --- a/apps/backend/README.md +++ b/apps/backend/README.md @@ -3,14 +3,14 @@ This is the dedicated backend for [AntAlmanac](https://antalmanac.com), which is primarily responsible for managing user data and internal information. -This is ___NOT___ for retrieving enrollment data from UCI; +This is **_NOT_** for retrieving enrollment data from UCI; [PeterPortal API](https://api.peterportal.org) is a separate ICSSC project dedicated to providing us this information. - -# Development +# Development ## Non-Privileged + When developing as a non-privileged member, the environment variables won't reflect real credentials to resources such as the database. @@ -19,37 +19,39 @@ Please request credentials from a project lead if you need them. 1. Ensure that you're in the backend project. i.e. `cd apps/backend` from the project root. 1. Change the `.env.sample` to `.env`. -2. Start the server with `pnpm start`. +1. Start the server with `pnpm start`. ## Privileged + ICSSC Project Committee Members can be given `.env` files with real credentials upon request. These can be used to access real resources such as DynamoDB, MapBox, etc. Remove any `.env.*` files in the project root, and insert the `.env` you were given. - # Architecture ## tRPC Routing (TODO) + We're currently migrating to [tRPC](https://trpc.io) and thus deprecating the previous REST based architecture. The desired functionality of the backend is still documented below. ## REST Routing (Deprecating) + The backend provides the following functionality. -- `/banners` -Returns the ads displayed above course search results. +- `/banners` + Returns the ads displayed above course search results. -- `/news` -Returns a list of news announcements displayed on the top right navbar. +- `/news` + Returns a list of news announcements displayed on the top right navbar. -- `/notifications` -Used to register for class notifications. +- `/notifications` + Used to register for class notifications. -- `/users` -Saves and returns user schedules. +- `/users` + Saves and returns user schedules. -- `/enrollmentData` -Returns information about course enrollment from previous terms. -(Legacy - this information is provided by PeterPortal API) +- `/enrollmentData` + Returns information about course enrollment from previous terms. + (Legacy - this information is provided by PeterPortal API) diff --git a/apps/backend/scripts/build.mjs b/apps/backend/scripts/build.mjs index 5a59ecd3d..46c619122 100644 --- a/apps/backend/scripts/build.mjs +++ b/apps/backend/scripts/build.mjs @@ -1,15 +1,15 @@ -import { build } from 'esbuild' +import { build } from 'esbuild'; async function main() { - await build({ - bundle: true, - minify: true, - platform: 'node', - outdir: 'dist', - entryPoints: { - lambda: 'src/lambda.ts', - } - }) + await build({ + bundle: true, + minify: true, + platform: 'node', + outdir: 'dist', + entryPoints: { + lambda: 'src/lambda.ts', + }, + }); } -main() +main(); diff --git a/apps/backend/src/db/ddb.ts b/apps/backend/src/db/ddb.ts index b26f6e72a..62a8ebc41 100644 --- a/apps/backend/src/db/ddb.ts +++ b/apps/backend/src/db/ddb.ts @@ -171,7 +171,6 @@ class DDBClient>> { } else { return parsedUserData.data; } - } } diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index bb1e2a356..065c1dc94 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -33,16 +33,18 @@ export async function start(corsEnabled = false) { app.use('/mapbox/tiles/*', async (req, res) => { const searchParams = new URLSearchParams(req.query as any); searchParams.set('access_token', env.MAPBOX_ACCESS_TOKEN); - const url = `${MAPBOX_API_URL}/styles/v1/mapbox/streets-v11/tiles/${(req.params as any)[0]}?${searchParams.toString()}`; + const url = `${MAPBOX_API_URL}/styles/v1/mapbox/streets-v11/tiles/${ + (req.params as any)[0] + }?${searchParams.toString()}`; const buffer = await fetch(url).then((res) => res.arrayBuffer()); - res.type('image/png') - res.send(Buffer.from(buffer)) + res.type('image/png'); + res.send(Buffer.from(buffer)); // // res.header('Content-Security-Policy', "img-src 'self'"); // https://stackoverflow.com/questions/56386307/loading-of-a-resource-blocked-by-content-security-policy // // res.header('Access-Control-Allow-Methods', 'GET, OPTIONS') // res.type('image/png') // res.send(result) }); - + app.use( '/trpc', createExpressMiddleware({ diff --git a/apps/backend/src/lambda.ts b/apps/backend/src/lambda.ts index 7df5df2f9..05609fd91 100644 --- a/apps/backend/src/lambda.ts +++ b/apps/backend/src/lambda.ts @@ -1,8 +1,8 @@ import serverlessExpress from '@vendia/serverless-express'; import type { Context, Handler } from 'aws-lambda'; +import env from './env'; import { start } from '.'; import connectToMongoDB from '$db/mongodb'; -import env from './env'; let cachedHandler: Handler; diff --git a/apps/backend/src/routers/course.ts b/apps/backend/src/routers/course.ts index 3e326d00b..53fd2f6e9 100644 --- a/apps/backend/src/routers/course.ts +++ b/apps/backend/src/routers/course.ts @@ -1,17 +1,17 @@ -import {z} from "zod"; +import { z } from 'zod'; import type { Course } from '@packages/antalmanac-types'; -import {procedure, router} from "../trpc"; +import { procedure, router } from '../trpc'; const courseRouter = router({ - get: procedure - .input(z.object({ id: z.string() })) - .query(async ({ input }) => { - return await fetch(`https://anteaterapi.com/v2/rest/courses/${encodeURIComponent(input.id)}`, { - headers: { - ...process.env.ANTEATER_API_KEY && { Authorization: `Bearer ${process.env.ANTEATER_API_KEY}`} - } - }).then(data => data.json()).then(data => data.ok ? data.data as Course : null); + get: procedure.input(z.object({ id: z.string() })).query(async ({ input }) => { + return await fetch(`https://anteaterapi.com/v2/rest/courses/${encodeURIComponent(input.id)}`, { + headers: { + ...(process.env.ANTEATER_API_KEY && { Authorization: `Bearer ${process.env.ANTEATER_API_KEY}` }), + }, }) + .then((data) => data.json()) + .then((data) => (data.ok ? (data.data as Course) : null)); + }), }); export default courseRouter; diff --git a/apps/backend/src/routers/enrollHist.ts b/apps/backend/src/routers/enrollHist.ts index 92fbbfda1..ef6c0bea6 100644 --- a/apps/backend/src/routers/enrollHist.ts +++ b/apps/backend/src/routers/enrollHist.ts @@ -1,11 +1,18 @@ import { EnrollmentHistory } from '@packages/antalmanac-types'; -import {z} from "zod"; -import {procedure, router} from "../trpc"; +import { z } from 'zod'; +import { procedure, router } from '../trpc'; const enrollHistRouter = router({ - get: procedure - .input(z.object({ department: z.string(), courseNumber: z.string(), sectionType: z.string() })) - .query(async ({ input }) => await fetch(`https://anteaterapi.com/v2/rest/enrollmentHistory?${new URLSearchParams(input)}`).then(x => x.json()).then(x => x.data as EnrollmentHistory)) + get: procedure.input(z.object({ department: z.string(), courseNumber: z.string(), sectionType: z.string() })).query( + async ({ input }) => + await fetch(`https://anteaterapi.com/v2/rest/enrollmentHistory?${new URLSearchParams(input)}`, { + headers: { + ...(process.env.ANTEATER_API_KEY && { Authorization: `Bearer ${process.env.ANTEATER_API_KEY}` }), + }, + }) + .then((x) => x.json()) + .then((x) => x.data as EnrollmentHistory) + ), }); export default enrollHistRouter; diff --git a/apps/backend/src/routers/grades.ts b/apps/backend/src/routers/grades.ts index 0c64524ef..51a985067 100644 --- a/apps/backend/src/routers/grades.ts +++ b/apps/backend/src/routers/grades.ts @@ -1,24 +1,53 @@ -import type {AggregateGrades, AggregateGradesByOffering} from '@packages/antalmanac-types'; -import {z} from "zod"; -import {procedure, router} from "../trpc"; +import type { AggregateGrades, AggregateGradesByOffering } from '@packages/antalmanac-types'; +import { z } from 'zod'; +import { procedure, router } from '../trpc'; const gradesRouter = router({ aggregateGrades: procedure - .input(z.object({ - department: z.string().optional(), - courseNumber: z.string().optional(), - instructor: z.string().optional(), - ge: z.string().optional() - })) - .query(async ({ input }) => await fetch(`https://anteaterapi.com/v2/rest/grades/aggregate?${new URLSearchParams(input)}`).then(x => x.json()).then(x => x.data as AggregateGrades)), + .input( + z.object({ + department: z.string().optional(), + courseNumber: z.string().optional(), + instructor: z.string().optional(), + ge: z.string().optional(), + }) + ) + .query( + async ({ input }) => + await fetch(`https://anteaterapi.com/v2/rest/grades/aggregate?${new URLSearchParams(input)}`, { + headers: { + ...(process.env.ANTEATER_API_KEY && { + Authorization: `Bearer ${process.env.ANTEATER_API_KEY}`, + }), + }, + }) + .then((x) => x.json()) + .then((x) => x.data as AggregateGrades) + ), aggregateByOffering: procedure - .input(z.object({ - department: z.string().optional(), - courseNumber: z.string().optional(), - instructor: z.string().optional(), - ge: z.string().optional() - })) - .query(async ({ input }) => await fetch(`https://anteaterapi.com/v2/rest/grades/aggregateByOffering?${new URLSearchParams(input)}`).then(x => x.json()).then(x => x.data as AggregateGradesByOffering)) + .input( + z.object({ + department: z.string().optional(), + courseNumber: z.string().optional(), + instructor: z.string().optional(), + ge: z.string().optional(), + }) + ) + .query( + async ({ input }) => + await fetch( + `https://anteaterapi.com/v2/rest/grades/aggregateByOffering?${new URLSearchParams(input)}`, + { + headers: { + ...(process.env.ANTEATER_API_KEY && { + Authorization: `Bearer ${process.env.ANTEATER_API_KEY}`, + }), + }, + } + ) + .then((x) => x.json()) + .then((x) => x.data as AggregateGradesByOffering) + ), }); export default gradesRouter; diff --git a/apps/backend/src/routers/index.ts b/apps/backend/src/routers/index.ts index c0a47df21..4545664fe 100644 --- a/apps/backend/src/routers/index.ts +++ b/apps/backend/src/routers/index.ts @@ -1,11 +1,11 @@ import { router } from '../trpc'; import newsRouter from './news'; import usersRouter from './users'; -import zotcourseRouter from "./zotcours"; -import courseRouter from "./course"; -import websocRouter from "./websoc"; -import gradesRouter from "./grades"; -import enrollHistRouter from "./enrollHist"; +import zotcourseRouter from './zotcours'; +import courseRouter from './course'; +import websocRouter from './websoc'; +import gradesRouter from './grades'; +import enrollHistRouter from './enrollHist'; const appRouter = router({ course: courseRouter, @@ -14,7 +14,7 @@ const appRouter = router({ news: newsRouter, users: usersRouter, websoc: websocRouter, - zotcourse: zotcourseRouter + zotcourse: zotcourseRouter, }); export type AppRouter = typeof appRouter; diff --git a/apps/backend/src/routers/users.ts b/apps/backend/src/routers/users.ts index 82d09c5c3..b15c6b9d1 100644 --- a/apps/backend/src/routers/users.ts +++ b/apps/backend/src/routers/users.ts @@ -1,8 +1,8 @@ import { type } from 'arktype'; import { UserSchema } from '@packages/antalmanac-types'; +import { TRPCError } from '@trpc/server'; import { router, procedure } from '../trpc'; import { ddbClient, VISIBILITY } from '../db/ddb'; -import { TRPCError } from '@trpc/server'; const userInputSchema = type([{ userId: 'string' }, '|', { googleId: 'string' }]); diff --git a/apps/backend/src/routers/websoc.ts b/apps/backend/src/routers/websoc.ts index f430912a1..50f3cede3 100644 --- a/apps/backend/src/routers/websoc.ts +++ b/apps/backend/src/routers/websoc.ts @@ -1,7 +1,7 @@ -import {z} from "zod"; +import { z } from 'zod'; import type { WebsocAPIResponse } from '@packages/antalmanac-types'; -import {procedure, router} from "../trpc"; -import type {CourseInfo} from "$aa/src/lib/course_data.types"; +import { procedure, router } from '../trpc'; +import type { CourseInfo } from '$aa/src/lib/course_data.types'; function cleanSearchParams(record: Record) { if ('term' in record) { @@ -14,19 +14,12 @@ function cleanSearchParams(record: Record) { record['year'] = year; } } - if ('startTime' in record) { - if (record['startTime'] === '') { - delete record['startTime']; - } - } - if ('endTime' in record) { - if (record['endTime'] === '') { - delete record['endTime']; - } + if ('department' in record) { + record['department'] = record['department'].toUpperCase(); } - if ('division' in record) { - if (record['division'] === '') { - delete record['division']; + for (const [key, value] of Object.entries(record)) { + if (value === '') { + delete record[key]; } } return record; @@ -76,9 +69,7 @@ function combineSOCObjects(SOCObjects: WebsocAPIResponse[]) { } const websocRouter = router({ - getOne: procedure - .input(z.record(z.string(), z.string())) - .query(queryWebSoc), + getOne: procedure.input(z.record(z.string(), z.string())).query(queryWebSoc), getMany: procedure .input(z.object({ params: z.record(z.string(), z.string()), fieldName: z.string() })) .query(async ({ input }) => { @@ -90,31 +81,29 @@ const websocRouter = router({ } return combineSOCObjects(responses); }), - getCourseInfo: procedure - .input(z.record(z.string(), z.string())) - .query(async ({ input }) => { - const res = await queryWebSoc({ input }); - const courseInfo: { [sectionCode: string]: CourseInfo } = {}; - for (const school of res.schools) { - for (const department of school.departments) { - for (const course of department.courses) { - for (const section of course.sections) { - courseInfo[section.sectionCode] = { - courseDetails: { - deptCode: department.deptCode, - courseNumber: course.courseNumber, - courseTitle: course.courseTitle, - courseComment: course.courseComment, - prerequisiteLink: course.prerequisiteLink, - }, - section: section, - }; - } + getCourseInfo: procedure.input(z.record(z.string(), z.string())).query(async ({ input }) => { + const res = await queryWebSoc({ input }); + const courseInfo: { [sectionCode: string]: CourseInfo } = {}; + for (const school of res.schools) { + for (const department of school.departments) { + for (const course of department.courses) { + for (const section of course.sections) { + courseInfo[section.sectionCode] = { + courseDetails: { + deptCode: department.deptCode, + courseNumber: course.courseNumber, + courseTitle: course.courseTitle, + courseComment: course.courseComment, + prerequisiteLink: course.prerequisiteLink, + }, + section: section, + }; } } } - return courseInfo; - }) -}) + } + return courseInfo; + }), +}); export default websocRouter; diff --git a/apps/backend/src/routers/zotcours.ts b/apps/backend/src/routers/zotcours.ts index 9ce5b1dae..31f8f668a 100644 --- a/apps/backend/src/routers/zotcours.ts +++ b/apps/backend/src/routers/zotcours.ts @@ -1,17 +1,15 @@ +import { type } from 'arktype'; import { procedure, router } from '../trpc'; -import {type} from 'arktype'; const zotcourseUrl = 'https://zotcourse.appspot.com/schedule/load'; const zotcourseRouter = router({ - getUserData: procedure - .input(type({ scheduleName: 'string' }).assert) - .mutation(async ({ input }) => { - let url = new URL(zotcourseUrl); - url.searchParams.append('username', input.scheduleName); - const response = await fetch(url); - return await response.json(); - }), + getUserData: procedure.input(type({ scheduleName: 'string' }).assert).mutation(async ({ input }) => { + const url = new URL(zotcourseUrl); + url.searchParams.append('username', input.scheduleName); + const response = await fetch(url); + return await response.json(); + }), }); -export default zotcourseRouter; \ No newline at end of file +export default zotcourseRouter; diff --git a/packages/types/src/schedule.ts b/packages/types/src/schedule.ts index f6ec4e233..169e663e9 100644 --- a/packages/types/src/schedule.ts +++ b/packages/types/src/schedule.ts @@ -1,6 +1,6 @@ import { type, arrayOf } from 'arktype'; import { RepeatingCustomEventSchema } from './customevent'; -import { AASectionSchema } from './websoc'; +import { AASection } from './websoc'; export const ScheduleCourseSchema = type({ courseComment: 'string', @@ -8,10 +8,10 @@ export const ScheduleCourseSchema = type({ courseTitle: 'string', deptCode: 'string', prerequisiteLink: 'string', - section: AASectionSchema, + section: 'object', term: 'string', }); -export type ScheduleCourse = typeof ScheduleCourseSchema.infer; +export type ScheduleCourse = Omit & { section: AASection }; export const ScheduleSchema = type({ scheduleName: 'string', From a94d08b0052c92d7ff54e13acd3bcb157d0647a2 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Mon, 11 Nov 2024 19:44:05 -0800 Subject: [PATCH 10/34] fix: misc --- .../SectionTable/SectionTableBody.tsx | 4 ++-- apps/antalmanac/src/lib/grades.ts | 2 +- apps/backend/src/routers/grades.ts | 24 ++++++++++++------- packages/anteater-api-types/src/websoc.ts | 2 ++ 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx index a840f9b38..7b6fd72cb 100644 --- a/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx +++ b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx @@ -12,7 +12,7 @@ import { } from '@material-ui/core'; import { withStyles } from '@material-ui/core/styles'; import { ClassNameMap, Styles } from '@material-ui/core/styles/withStyles'; -import { AASection, EnrollmentHistory, WebsocSectionMeeting } from '@packages/antalmanac-types'; +import { AASection, WebsocSectionEnrollment, WebsocSectionMeeting } from '@packages/antalmanac-types'; import classNames from 'classnames'; import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; @@ -343,7 +343,7 @@ const LocationsCell = withStyles(styles)((props: LocationsCellProps) => { interface SectionEnrollmentCellProps { classes: ClassNameMap; - numCurrentlyEnrolled: EnrollmentHistory; + numCurrentlyEnrolled: WebsocSectionEnrollment; maxCapacity: number; /** diff --git a/apps/antalmanac/src/lib/grades.ts b/apps/antalmanac/src/lib/grades.ts index f5fb91c1e..e73f66989 100644 --- a/apps/antalmanac/src/lib/grades.ts +++ b/apps/antalmanac/src/lib/grades.ts @@ -55,7 +55,7 @@ class _Grades { // If the whole query has already been cached, return if (this.cachedQueries.has(queryKey)) return; - const groupedGrades = await trpc.grades.aggregateByOffering.query({ department, ge }); + const groupedGrades = await trpc.grades.aggregateByOffering.mutate({ department, ge }); console.log(groupedGrades); diff --git a/apps/backend/src/routers/grades.ts b/apps/backend/src/routers/grades.ts index 51a985067..b1a4623fc 100644 --- a/apps/backend/src/routers/grades.ts +++ b/apps/backend/src/routers/grades.ts @@ -24,19 +24,27 @@ const gradesRouter = router({ .then((x) => x.json()) .then((x) => x.data as AggregateGrades) ), + // This is a "mutation" because we don't want tRPC to batch it with the query for WebSoc data. aggregateByOffering: procedure .input( - z.object({ - department: z.string().optional(), - courseNumber: z.string().optional(), - instructor: z.string().optional(), - ge: z.string().optional(), - }) + z + .object({ + department: z.string().optional(), + courseNumber: z.string().optional(), + instructor: z.string().optional(), + ge: z.string().optional(), + }) + .transform(({ department, ge, ...rest }) => { + const dept = department?.toUpperCase(); + return ge === undefined ? { department: dept, ...rest } : { department: dept, ge, ...rest }; + }) ) - .query( + .mutation( async ({ input }) => await fetch( - `https://anteaterapi.com/v2/rest/grades/aggregateByOffering?${new URLSearchParams(input)}`, + `https://anteaterapi.com/v2/rest/grades/aggregateByOffering?${new URLSearchParams( + input as Record + )}`, { headers: { ...(process.env.ANTEATER_API_KEY && { diff --git a/packages/anteater-api-types/src/websoc.ts b/packages/anteater-api-types/src/websoc.ts index 68344e66d..b5a0e399b 100644 --- a/packages/anteater-api-types/src/websoc.ts +++ b/packages/anteater-api-types/src/websoc.ts @@ -7,6 +7,8 @@ export type WebsocCourse = WebsocAPIResponse['schools'][number]['departments'][n export type WebsocSection = WebsocCourse['sections'][number]; +export type WebsocSectionEnrollment = WebsocSection['numCurrentlyEnrolled']; + export type WebsocSectionMeeting = WebsocSection['meetings'][number]; export type WebsocSectionFinalExam = WebsocSection['finalExam']; From b1436e1983dc7c40d6a52cfeb1cd23ec4cb2238e Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Mon, 11 Nov 2024 20:35:55 -0800 Subject: [PATCH 11/34] 99 typescript errors in the code, 99 ts errors take one down pass it around 210 typescript errors in the code --- .../antalmanac/src/actions/AppStoreActions.ts | 2 +- .../Calendar/CourseCalendarEvent.tsx | 21 ++----- .../src/components/Header/Import.tsx | 2 +- .../AddedCourses/AddedCoursePane.tsx | 1 + .../RightPane/SectionTable/GradesPopup.tsx | 5 +- .../RightPane/SectionTable/PrereqTree.tsx | 10 ++- .../SectionTable/SectionTableBody.tsx | 3 +- .../RightPane/SectionTable/cells/action.tsx | 2 +- apps/antalmanac/src/lib/download.ts | 2 +- .../src/lib/tourExampleGeneration.ts | 32 ++++------ apps/antalmanac/src/stores/HoveredStore.ts | 2 +- apps/antalmanac/src/stores/Schedules.ts | 2 +- .../src/stores/calendarizeHelpers.ts | 61 ++++++++++++------- .../tests/calendarize-helpers.test.ts | 8 ++- apps/antalmanac/tests/termData.tsx | 6 -- apps/antalmanac/vite.config.ts | 1 + apps/backend/src/routers/websoc.ts | 3 +- packages/anteater-api-types/src/websoc.ts | 6 +- .../types/src/courseData.ts | 2 +- packages/types/src/index.ts | 1 + packages/types/src/schedule.ts | 43 ++++++------- packages/types/src/websoc.ts | 4 +- 22 files changed, 111 insertions(+), 108 deletions(-) rename apps/antalmanac/src/lib/course_data.types.ts => packages/types/src/courseData.ts (80%) diff --git a/apps/antalmanac/src/actions/AppStoreActions.ts b/apps/antalmanac/src/actions/AppStoreActions.ts index e98046acf..78bbf2c55 100644 --- a/apps/antalmanac/src/actions/AppStoreActions.ts +++ b/apps/antalmanac/src/actions/AppStoreActions.ts @@ -1,11 +1,11 @@ import { RepeatingCustomEvent, ScheduleCourse, ShortCourseSchedule, WebsocSection } from '@packages/antalmanac-types'; +import { CourseDetails } from '@packages/antalmanac-types'; import { TRPCError } from '@trpc/server'; import { VariantType } from 'notistack'; import { SnackbarPosition } from '$components/NotificationSnackbar'; import analyticsEnum, { logAnalytics, courseNumAsDecimal } from '$lib/analytics'; import trpc from '$lib/api/trpc'; -import { CourseDetails } from '$lib/course_data.types'; import { warnMultipleTerms } from '$lib/helpers'; import { removeLocalStorageUserId, setLocalStorageUserId } from '$lib/localStorage'; import AppStore from '$stores/AppStore'; diff --git a/apps/antalmanac/src/components/Calendar/CourseCalendarEvent.tsx b/apps/antalmanac/src/components/Calendar/CourseCalendarEvent.tsx index 6930321af..f1636ce66 100644 --- a/apps/antalmanac/src/components/Calendar/CourseCalendarEvent.tsx +++ b/apps/antalmanac/src/components/Calendar/CourseCalendarEvent.tsx @@ -2,6 +2,7 @@ import { Chip, IconButton, Paper, Tooltip } from '@material-ui/core'; import { Theme, withStyles } from '@material-ui/core/styles'; import { ClassNameMap, Styles } from '@material-ui/core/styles/withStyles'; import { Delete } from '@material-ui/icons'; +import { WebsocSectionFinalExam } from '@packages/antalmanac-types'; import { useEffect, useRef, useCallback } from 'react'; import { Event } from 'react-big-calendar'; import { Link } from 'react-router-dom'; @@ -107,21 +108,9 @@ export interface Location { days?: string; } -export interface FinalExam { - examStatus: 'NO_FINAL' | 'TBA_FINAL' | 'SCHEDULED_FINAL'; - dayOfWeek: 'Sun' | 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' | null; - month: number | null; - day: number | null; - startTime: { - hour: number; - minute: number; - } | null; - endTime: { - hour: number; - minute: number; - } | null; - locations: Location[] | null; -} +export type FinalExam = + | (Omit, 'bldg'> & { locations: Location[] }) + | Extract; export interface CourseEvent extends CommonCalendarEvent { locations: Location[]; @@ -195,7 +184,7 @@ const CourseCalendarEvent = (props: CourseCalendarEventProps) => { } else if (finalExam.examStatus == 'TBA_FINAL') { finalExamString = 'Final TBA'; } else { - if (finalExam.startTime && finalExam.endTime && finalExam.month && finalExam.locations) { + if (finalExam.examStatus === 'SCHEDULED_FINAL') { const timeString = formatTimes(finalExam.startTime, finalExam.endTime, isMilitaryTime); const locationString = `at ${finalExam.locations .map((location) => `${location.building} ${location.room}`) diff --git a/apps/antalmanac/src/components/Header/Import.tsx b/apps/antalmanac/src/components/Header/Import.tsx index 6eee0cb66..91abf0253 100644 --- a/apps/antalmanac/src/components/Header/Import.tsx +++ b/apps/antalmanac/src/components/Header/Import.tsx @@ -15,6 +15,7 @@ import { } from '@material-ui/core'; import InputLabel from '@material-ui/core/InputLabel'; import { PostAdd } from '@material-ui/icons'; +import { CourseInfo } from '@packages/antalmanac-types'; import { ChangeEvent, useCallback, useEffect, useState } from 'react'; import TermSelector from '../RightPane/CoursePane/SearchForm/TermSelector'; @@ -22,7 +23,6 @@ import RightPaneStore from '../RightPane/RightPaneStore'; import { addCustomEvent, openSnackbar, addCourse } from '$actions/AppStoreActions'; import analyticsEnum, { logAnalytics } from '$lib/analytics'; -import { CourseInfo } from '$lib/course_data.types'; import { QueryZotcourseError } from '$lib/customErrors'; import { warnMultipleTerms } from '$lib/helpers'; import { WebSOC } from '$lib/websoc'; diff --git a/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx b/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx index f865fc17b..fa84b8419 100644 --- a/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx +++ b/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx @@ -65,6 +65,7 @@ function getCourses() { ...course.section, }, ], + updatedAt: null, }; formattedCourses.push(formattedCourse); } diff --git a/apps/antalmanac/src/components/RightPane/SectionTable/GradesPopup.tsx b/apps/antalmanac/src/components/RightPane/SectionTable/GradesPopup.tsx index dca0900a3..f82c93d8d 100644 --- a/apps/antalmanac/src/components/RightPane/SectionTable/GradesPopup.tsx +++ b/apps/antalmanac/src/components/RightPane/SectionTable/GradesPopup.tsx @@ -72,7 +72,10 @@ function GradesPopup(props: GradesPopupProps) { return gradeData ? `${deptCode} ${courseNumber}${ instructor ? ` — ${instructor}` : '' - } | Average GPA: ${gradeData.courseGrades.averageGPA.toFixed(2)}` + // GPA is `null` if the class is pass/no-pass only. + // This is more correct compared to returning a zero GPA, + // which so far has not happened, but is entirely possible. + } | Average GPA: ${gradeData.courseGrades.averageGPA?.toFixed(2) ?? 'n/a'}` : 'Grades are not available for this class.'; }, [gradeData, deptCode, courseNumber, instructor]); diff --git a/apps/antalmanac/src/components/RightPane/SectionTable/PrereqTree.tsx b/apps/antalmanac/src/components/RightPane/SectionTable/PrereqTree.tsx index 9e914f3c7..d027e5bcf 100644 --- a/apps/antalmanac/src/components/RightPane/SectionTable/PrereqTree.tsx +++ b/apps/antalmanac/src/components/RightPane/SectionTable/PrereqTree.tsx @@ -57,9 +57,13 @@ const PrereqTreeNode: FC = (props) => { return (
  • diff --git a/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx index 7b6fd72cb..dead2a3ba 100644 --- a/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx +++ b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTableBody.tsx @@ -12,7 +12,7 @@ import { } from '@material-ui/core'; import { withStyles } from '@material-ui/core/styles'; import { ClassNameMap, Styles } from '@material-ui/core/styles/withStyles'; -import { AASection, WebsocSectionEnrollment, WebsocSectionMeeting } from '@packages/antalmanac-types'; +import { AASection, WebsocSectionEnrollment, WebsocSectionMeeting, CourseDetails } from '@packages/antalmanac-types'; import classNames from 'classnames'; import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; @@ -25,7 +25,6 @@ import { SectionActionCell } from './cells/action'; import restrictionsMapping from './static/restrictionsMapping.json'; import analyticsEnum, { logAnalytics } from '$lib/analytics'; -import { CourseDetails } from '$lib/course_data.types'; import { Grades } from '$lib/grades'; import { clickToCopy } from '$lib/helpers'; import locationIds from '$lib/location_ids'; diff --git a/apps/antalmanac/src/components/RightPane/SectionTable/cells/action.tsx b/apps/antalmanac/src/components/RightPane/SectionTable/cells/action.tsx index efb67fc20..73a23bc56 100644 --- a/apps/antalmanac/src/components/RightPane/SectionTable/cells/action.tsx +++ b/apps/antalmanac/src/components/RightPane/SectionTable/cells/action.tsx @@ -1,6 +1,7 @@ import { Add, ArrowDropDown, Delete } from '@mui/icons-material'; import { Box, IconButton, Menu, MenuItem, TableCell, Tooltip, useMediaQuery } from '@mui/material'; import { AASection } from '@packages/antalmanac-types'; +import { CourseDetails } from '@packages/antalmanac-types'; import { bindMenu, bindTrigger, usePopupState } from 'material-ui-popup-state/hooks'; import { MOBILE_BREAKPOINT } from '../../../../globals'; @@ -8,7 +9,6 @@ import { MOBILE_BREAKPOINT } from '../../../../globals'; import { addCourse, deleteCourse, openSnackbar } from '$actions/AppStoreActions'; import ColorPicker from '$components/ColorPicker'; import analyticsEnum, { logAnalytics } from '$lib/analytics'; -import { CourseDetails } from '$lib/course_data.types'; import AppStore from '$stores/AppStore'; /** diff --git a/apps/antalmanac/src/lib/download.ts b/apps/antalmanac/src/lib/download.ts index 05488c962..d211ee307 100644 --- a/apps/antalmanac/src/lib/download.ts +++ b/apps/antalmanac/src/lib/download.ts @@ -162,7 +162,7 @@ export function getFirstClass( * ``` */ export function getExamTime(exam: FinalExam, year: number): [DateTimeArray, DateTimeArray] | [] { - if (exam.month && exam.day && exam.startTime && exam.endTime) { + if (exam.examStatus === 'SCHEDULED_FINAL') { const month = exam.month; const day = exam.day; const [examStartTime, examEndTime] = parseTimes(exam.startTime, exam.endTime); diff --git a/apps/antalmanac/src/lib/tourExampleGeneration.ts b/apps/antalmanac/src/lib/tourExampleGeneration.ts index bed345940..fae57f0ff 100644 --- a/apps/antalmanac/src/lib/tourExampleGeneration.ts +++ b/apps/antalmanac/src/lib/tourExampleGeneration.ts @@ -123,6 +123,10 @@ function randomStartEndTime(duration: number): [HourMinute, HourMinute] { return [start, end]; } +type NonStrictPartialWebsocSectionMeeting = Partial> & { + timeIsTBA?: boolean; +}; + export function sampleMeetingsFactory({ bldg = ['DBH 1200'], days = 'MWF', @@ -135,7 +139,7 @@ export function sampleMeetingsFactory({ minute: 50, }, timeIsTBA = false, -}: Partial): WebsocSectionMeeting[] { +}: NonStrictPartialWebsocSectionMeeting): WebsocSectionMeeting[] { return [ { bldg, @@ -147,6 +151,10 @@ export function sampleMeetingsFactory({ ]; } +type NonStrictPartialWebsocSectionFinalExam = Partial< + Omit, 'examStatus'> +> & { examStatus?: WebsocSectionFinalExam['examStatus'] }; + export function sampleFinalExamFactory({ examStatus = 'SCHEDULED_FINAL', dayOfWeek, @@ -155,23 +163,8 @@ export function sampleFinalExamFactory({ startTime, endTime, bldg = ['DBH'], -}: Partial): WebsocSectionFinalExam { - if (examStatus == 'NO_FINAL') - return { - examStatus, - dayOfWeek: 'Mon', - month: 0, - day: 0, - startTime: { - hour: 0, - minute: 0, - }, - endTime: { - hour: 0, - minute: 0, - }, - bldg, - }; +}: NonStrictPartialWebsocSectionFinalExam): WebsocSectionFinalExam { + if (examStatus === 'NO_FINAL') return { examStatus }; const [randomStartTime, randomEndTime] = randomStartEndTime(120); startTime = startTime ?? randomStartTime; @@ -232,9 +225,10 @@ export function sampleClassFactory({ sectionCode: randint(10000, 99999).toString(), sectionComment: '', sectionNum: '1', - sectionType: 'LEC', + sectionType: 'Lec', status: 'Waitl', units: '4', + updatedAt: null, }, }; } diff --git a/apps/antalmanac/src/stores/HoveredStore.ts b/apps/antalmanac/src/stores/HoveredStore.ts index 7b2d60a7e..66b001116 100644 --- a/apps/antalmanac/src/stores/HoveredStore.ts +++ b/apps/antalmanac/src/stores/HoveredStore.ts @@ -1,10 +1,10 @@ import { AASection, ScheduleCourse } from '@packages/antalmanac-types'; +import { CourseDetails } from '@packages/antalmanac-types'; import { create } from 'zustand'; import { calendarizeCourseEvents, calendarizeFinals } from './calendarizeHelpers'; import { CourseEvent } from '$components/Calendar/CourseCalendarEvent'; -import { CourseDetails } from '$lib/course_data.types'; const HOVERED_SECTION_COLOR = '#80808080'; export interface HoveredStore { diff --git a/apps/antalmanac/src/stores/Schedules.ts b/apps/antalmanac/src/stores/Schedules.ts index c76e1975a..0129082ff 100644 --- a/apps/antalmanac/src/stores/Schedules.ts +++ b/apps/antalmanac/src/stores/Schedules.ts @@ -6,10 +6,10 @@ import type { ShortCourseSchedule, RepeatingCustomEvent, } from '@packages/antalmanac-types'; +import type { CourseInfo } from '@packages/antalmanac-types'; import { calendarizeCourseEvents, calendarizeCustomEvents, calendarizeFinals } from './calendarizeHelpers'; -import type { CourseInfo } from '$lib/course_data.types'; import { termData } from '$lib/termData'; import { WebSOC } from '$lib/websoc'; import { getColorForNewSection } from '$stores/scheduleHelpers'; diff --git a/apps/antalmanac/src/stores/calendarizeHelpers.ts b/apps/antalmanac/src/stores/calendarizeHelpers.ts index 75c5eef44..7dc2280f0 100644 --- a/apps/antalmanac/src/stores/calendarizeHelpers.ts +++ b/apps/antalmanac/src/stores/calendarizeHelpers.ts @@ -1,4 +1,9 @@ -import type { ScheduleCourse, RepeatingCustomEvent, HourMinute } from '@packages/antalmanac-types'; +import type { + ScheduleCourse, + RepeatingCustomEvent, + HourMinute, + WebsocSectionFinalExam, +} from '@packages/antalmanac-types'; import { CourseEvent, CustomEvent, Location } from '$components/Calendar/CourseCalendarEvent'; import { getFinalsStartForTerm } from '$lib/termData'; @@ -16,12 +21,12 @@ export function getLocation(location: string): Location { export function calendarizeCourseEvents(currentCourses: ScheduleCourse[] = []): CourseEvent[] { return currentCourses.flatMap((course) => { return course.section.meetings - .filter((meeting) => !meeting.timeIsTBA && meeting.startTime && meeting.endTime && meeting.days) + .filter((meeting) => !meeting.timeIsTBA) .flatMap((meeting) => { - const startHour = meeting.startTime?.hour; - const startMin = meeting.startTime?.minute; - const endHour = meeting.endTime?.hour; - const endMin = meeting.endTime?.minute; + const startHour = meeting.startTime.hour; + const startMin = meeting.startTime.minute; + const endHour = meeting.endTime.hour; + const endMin = meeting.endTime.minute; /** * An array of booleans indicating whether a course meeting occurs on that day. @@ -40,7 +45,10 @@ export function calendarizeCourseEvents(currentCourses: ScheduleCourse[] = []): .filter(notNull); // Intermediate formatting to subtract `bldg` attribute in favor of `locations` - const { bldg: _, ...finalExam } = course.section.finalExam; + const { bldg: _, ...finalExam } = + course.section.finalExam.examStatus === 'SCHEDULED_FINAL' + ? course.section.finalExam + : { bldg: '', examStatus: course.section.finalExam.examStatus }; return dayIndicesOccurring.map((dayIndex) => { return { @@ -62,7 +70,10 @@ export function calendarizeCourseEvents(currentCourses: ScheduleCourse[] = []): end: new Date(2018, 0, dayIndex, endHour, endMin), finalExam: { ...finalExam, - locations: course.section.finalExam.bldg?.map(getLocation) ?? [], + locations: + course.section.finalExam.examStatus === 'SCHEDULED_FINAL' + ? course.section.finalExam.bldg.map(getLocation) + : [], }, isCustomEvent: false, }; @@ -73,27 +84,27 @@ export function calendarizeCourseEvents(currentCourses: ScheduleCourse[] = []): export function calendarizeFinals(currentCourses: ScheduleCourse[] = []): CourseEvent[] { return currentCourses - .filter( - (course) => - course.section.finalExam.examStatus === 'SCHEDULED_FINAL' && - course.section.finalExam.startTime && - course.section.finalExam.endTime && - course.section.finalExam.dayOfWeek - ) + .filter((course) => course.section.finalExam.examStatus === 'SCHEDULED_FINAL') .flatMap((course) => { - const { bldg, ...finalExam } = course.section.finalExam; - - const startHour = finalExam.startTime?.hour; - const startMin = finalExam.startTime?.minute; - const endHour = finalExam.endTime?.hour; - const endMin = finalExam.endTime?.minute; + // This assertion is only necessary because the filter above is not actually a type guard for the finalExam object. + // I guess because it's an attribute of another attribute? TypeScript pls + const finalExamObject = course.section.finalExam as Extract< + WebsocSectionFinalExam, + { examStatus: 'SCHEDULED_FINAL' } + >; + const { bldg, ...finalExam } = finalExamObject; + + const startHour = finalExam.startTime.hour; + const startMin = finalExam.startTime.minute; + const endHour = finalExam.endTime.hour; + const endMin = finalExam.endTime.minute; /** * An array of booleans indicating whether the day at that index is a day that the final. * * @example [false, false, false, true, false, true, false], i.e. [T, Th] */ - const weekdaysOccurring = getReferencesOccurring(FINALS_WEEK_DAYS, course.section.finalExam.dayOfWeek); + const weekdaysOccurring = getReferencesOccurring(FINALS_WEEK_DAYS, finalExam.dayOfWeek); /** * Only include the day indices that the final is occurring. @@ -104,7 +115,11 @@ export function calendarizeFinals(currentCourses: ScheduleCourse[] = []): Course .map((day, index) => (day ? index : undefined)) .filter(notNull); - const locationsWithNoDays = bldg ? bldg.map(getLocation) : course.section.meetings[0].bldg.map(getLocation); + const locationsWithNoDays = bldg + ? bldg.map(getLocation) + : !course.section.meetings[0].timeIsTBA + ? course.section.meetings[0].bldg.map(getLocation) + : []; /** * Fallback to January 2018 if no finals start date is available. diff --git a/apps/antalmanac/tests/calendarize-helpers.test.ts b/apps/antalmanac/tests/calendarize-helpers.test.ts index f52ff42a9..13ae4b2dd 100644 --- a/apps/antalmanac/tests/calendarize-helpers.test.ts +++ b/apps/antalmanac/tests/calendarize-helpers.test.ts @@ -1,7 +1,8 @@ -import { describe, test, expect } from 'vitest'; import type { Schedule, RepeatingCustomEvent } from '@packages/antalmanac-types'; -import { calendarizeCourseEvents, calendarizeCustomEvents, calendarizeFinals } from '$stores/calendarizeHelpers'; +import { describe, test, expect } from 'vitest'; + import type { CourseEvent, CustomEvent } from '$components/Calendar/CourseCalendarEvent'; +import { calendarizeCourseEvents, calendarizeCustomEvents, calendarizeFinals } from '$stores/calendarizeHelpers'; describe('calendarize-helpers', () => { const courses: Schedule['courses'] = [ @@ -14,7 +15,7 @@ describe('calendarize-helpers', () => { section: { color: 'placeholderColor', sectionCode: 'placeholderSectionCode', - sectionType: 'placeholderSectionType', + sectionType: 'Lec', sectionNum: 'placeholderSectionNum', units: 'placeholderUnits', instructors: [], @@ -60,6 +61,7 @@ describe('calendarize-helpers', () => { restrictions: 'placeholderRestrictions', status: 'OPEN', sectionComment: 'placeholderSectionComment', + updatedAt: 'placeholderUpdatedAt', }, term: '2024 Winter', }, diff --git a/apps/antalmanac/tests/termData.tsx b/apps/antalmanac/tests/termData.tsx index b2f48c2dc..52089f94b 100644 --- a/apps/antalmanac/tests/termData.tsx +++ b/apps/antalmanac/tests/termData.tsx @@ -18,12 +18,6 @@ describe('termData', () => { showLocationInfo: false, finalExam: { examStatus: 'NO_FINAL', - dayOfWeek: 'Sun', - month: 0, - day: 0, - startTime: null, - endTime: null, - locations: null, }, courseTitle: '', instructors: [], diff --git a/apps/antalmanac/vite.config.ts b/apps/antalmanac/vite.config.ts index c6e03d07b..e2a7ae343 100644 --- a/apps/antalmanac/vite.config.ts +++ b/apps/antalmanac/vite.config.ts @@ -23,6 +23,7 @@ export default defineConfig({ server: { host: 'localhost', }, + // @ts-expect-error test: { environment: 'jsdom', setupFiles: [resolve(__dirname, 'tests/setup/setup.ts')], diff --git a/apps/backend/src/routers/websoc.ts b/apps/backend/src/routers/websoc.ts index 50f3cede3..c23c8df92 100644 --- a/apps/backend/src/routers/websoc.ts +++ b/apps/backend/src/routers/websoc.ts @@ -1,7 +1,6 @@ import { z } from 'zod'; -import type { WebsocAPIResponse } from '@packages/antalmanac-types'; +import type { WebsocAPIResponse, CourseInfo } from '@packages/antalmanac-types'; import { procedure, router } from '../trpc'; -import type { CourseInfo } from '$aa/src/lib/course_data.types'; function cleanSearchParams(record: Record) { if ('term' in record) { diff --git a/packages/anteater-api-types/src/websoc.ts b/packages/anteater-api-types/src/websoc.ts index b5a0e399b..85d6a07ca 100644 --- a/packages/anteater-api-types/src/websoc.ts +++ b/packages/anteater-api-types/src/websoc.ts @@ -3,7 +3,11 @@ import { paths } from './generated/anteater-api-types'; export type WebsocAPIResponse = paths['/v2/rest/websoc']['get']['responses'][200]['content']['application/json']['data']; -export type WebsocCourse = WebsocAPIResponse['schools'][number]['departments'][number]['courses'][number]; +export type WebsocSchool = WebsocAPIResponse['schools'][number]; + +export type WebsocDepartment = WebsocSchool['departments'][number]; + +export type WebsocCourse = WebsocDepartment['courses'][number]; export type WebsocSection = WebsocCourse['sections'][number]; diff --git a/apps/antalmanac/src/lib/course_data.types.ts b/packages/types/src/courseData.ts similarity index 80% rename from apps/antalmanac/src/lib/course_data.types.ts rename to packages/types/src/courseData.ts index 849e8082a..76455ad26 100644 --- a/apps/antalmanac/src/lib/course_data.types.ts +++ b/packages/types/src/courseData.ts @@ -1,4 +1,4 @@ -import { WebsocSection } from '@packages/antalmanac-types'; +import { WebsocSection } from '@packages/anteater-api-types'; export interface CourseDetails { deptCode: string; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 3576e8384..2c594c8d0 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,5 +1,6 @@ export * from './schedule'; export * from './customevent'; +export * from './courseData'; export * from './user'; export * from './legacy'; export * from './websoc'; diff --git a/packages/types/src/schedule.ts b/packages/types/src/schedule.ts index 169e663e9..63a5a139b 100644 --- a/packages/types/src/schedule.ts +++ b/packages/types/src/schedule.ts @@ -1,25 +1,23 @@ import { type, arrayOf } from 'arktype'; -import { RepeatingCustomEventSchema } from './customevent'; +import { RepeatingCustomEvent, RepeatingCustomEventSchema } from './customevent'; import { AASection } from './websoc'; -export const ScheduleCourseSchema = type({ - courseComment: 'string', - courseNumber: 'string', - courseTitle: 'string', - deptCode: 'string', - prerequisiteLink: 'string', - section: 'object', - term: 'string', -}); -export type ScheduleCourse = Omit & { section: AASection }; +export type ScheduleCourse = { + courseComment: string; + courseNumber: string; + courseTitle: string; + deptCode: string; + prerequisiteLink: string; + section: AASection; + term: string; +}; -export const ScheduleSchema = type({ - scheduleName: 'string', - courses: arrayOf(ScheduleCourseSchema), - customEvents: arrayOf(RepeatingCustomEventSchema), - scheduleNoteId: 'number', -}); -export type Schedule = typeof ScheduleSchema.infer; +export type Schedule = { + scheduleName: string; + courses: ScheduleCourse[]; + customEvents: RepeatingCustomEvent[]; + scheduleNoteId: number; +}; export const ShortCourseSchema = type({ color: 'string', @@ -46,8 +44,7 @@ export const ScheduleSaveStateSchema = type({ }); export type ScheduleSaveState = typeof ScheduleSaveStateSchema.infer; -export const ScheduleUndoStateSchema = type({ - schedules: arrayOf(ScheduleSchema), - scheduleIndex: 'number', -}); -export type ScheduleUndoState = typeof ScheduleUndoStateSchema.infer; +export type ScheduleUndoState = { + schedules: Schedule[]; + scheduleIndex: number; +}; diff --git a/packages/types/src/websoc.ts b/packages/types/src/websoc.ts index 8b7a60060..edabd1ae4 100644 --- a/packages/types/src/websoc.ts +++ b/packages/types/src/websoc.ts @@ -1,7 +1,7 @@ import { WebsocSection, WebsocCourse } from '@packages/anteater-api-types'; type AASectionExtendedProperties = { - color: 'string'; + color: string; }; export type AASection = WebsocSection & AASectionExtendedProperties; @@ -10,4 +10,4 @@ type AACourseExtendedProperties = { sections: AASection[]; }; -export type AACourse = WebsocCourse & AACourseExtendedProperties; +export type AACourse = Omit & AACourseExtendedProperties; From a9d20defac953f722ed7e7e78c502d3b4e1020ad Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Mon, 11 Nov 2024 20:45:57 -0800 Subject: [PATCH 12/34] random fixes --- apps/antalmanac/src/lib/grades.ts | 2 -- apps/backend/src/routers/enrollHist.ts | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/antalmanac/src/lib/grades.ts b/apps/antalmanac/src/lib/grades.ts index e73f66989..8123daf4a 100644 --- a/apps/antalmanac/src/lib/grades.ts +++ b/apps/antalmanac/src/lib/grades.ts @@ -57,8 +57,6 @@ class _Grades { const groupedGrades = await trpc.grades.aggregateByOffering.mutate({ department, ge }); - console.log(groupedGrades); - if (!groupedGrades) throw new Error('populateGradesCache: Failed to query grades'); // Populate cache diff --git a/apps/backend/src/routers/enrollHist.ts b/apps/backend/src/routers/enrollHist.ts index ef6c0bea6..44e9e4e69 100644 --- a/apps/backend/src/routers/enrollHist.ts +++ b/apps/backend/src/routers/enrollHist.ts @@ -12,6 +12,7 @@ const enrollHistRouter = router({ }) .then((x) => x.json()) .then((x) => x.data as EnrollmentHistory) + .then((xs) => xs.filter(x => x.dates.length)) // FIXME remove this shim once this is fixed on the API end ), }); From be259e652d44f653eed9a3e862070da6392f2770 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Mon, 11 Nov 2024 20:58:34 -0800 Subject: [PATCH 13/34] docs --- README.md | 2 +- apps/backend/README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bffba2022..2cd452a5d 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ A summary of the libraries we use are listed below. ### Backend - [tRPC](https://trpc.io) - type-safe API access layer for the AntAlmanac API. -- [PeterPortal API](https://api.peterportal.org) - API maintained by ICSSC for retrieving UCI data. +- [Anteater API](https://docs.icssc.club/developer/anteaterapi) - API maintained by ICSSC for retrieving UCI data. ### Tooling - [Vite](https://vitejs.dev) - Blazingly fast, modern bundler. diff --git a/apps/backend/README.md b/apps/backend/README.md index 2bccdf26a..8ca487323 100644 --- a/apps/backend/README.md +++ b/apps/backend/README.md @@ -4,7 +4,7 @@ This is the dedicated backend for [AntAlmanac](https://antalmanac.com), which is primarily responsible for managing user data and internal information. This is **_NOT_** for retrieving enrollment data from UCI; -[PeterPortal API](https://api.peterportal.org) is a separate ICSSC project dedicated +[Anteater API](https://docs.icssc.club/developer/anteaterapi) is a separate ICSSC project dedicated to providing us this information. # Development @@ -54,4 +54,4 @@ The backend provides the following functionality. - `/enrollmentData` Returns information about course enrollment from previous terms. - (Legacy - this information is provided by PeterPortal API) + (Legacy - this information is provided by Anteater API) From 0d3660aba21b81f98d24333a1d62b763066e5036 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:01:34 -0800 Subject: [PATCH 14/34] anteater api key for ci --- .github/workflows/deploy_production.yml | 1 + .github/workflows/deploy_staging.yml | 1 + apps/cdk/src/stacks/backend.ts | 2 ++ 3 files changed, 4 insertions(+) diff --git a/.github/workflows/deploy_production.yml b/.github/workflows/deploy_production.yml index b28501057..322c40567 100644 --- a/.github/workflows/deploy_production.yml +++ b/.github/workflows/deploy_production.yml @@ -28,6 +28,7 @@ env: VITE_TILES_ENDPOINT: ${{ secrets.VITE_TILES_ENDPOINT}} GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} + ANTEATER_API_KEY: ${{ secrets.ANTEATER_API_KEY }} # Turborepo credentials. TURBO_API: ${{ vars.TURBO_API }} diff --git a/.github/workflows/deploy_staging.yml b/.github/workflows/deploy_staging.yml index b514eea34..5ca8adcff 100644 --- a/.github/workflows/deploy_staging.yml +++ b/.github/workflows/deploy_staging.yml @@ -28,6 +28,7 @@ env: VITE_TILES_ENDPOINT: ${{ secrets.VITE_TILES_ENDPOINT}} GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} + ANTEATER_API_KEY: ${{ secrets.ANTEATER_API_KEY }} # Turborepo credentials. TURBO_API: ${{ vars.TURBO_API }} diff --git a/apps/cdk/src/stacks/backend.ts b/apps/cdk/src/stacks/backend.ts index 757f881a9..40395d80a 100644 --- a/apps/cdk/src/stacks/backend.ts +++ b/apps/cdk/src/stacks/backend.ts @@ -26,6 +26,7 @@ export class BackendStack extends Stack { 'MAPBOX_ACCESS_TOKEN?': 'string', 'NODE_ENV?': 'string', 'PR_NUM?': 'string', + ANTEATER_API_KEY: 'string', }).assert({ ...process.env }); /** @@ -55,6 +56,7 @@ export class BackendStack extends Stack { timeout: Duration.seconds(5), memorySize: 256, environment: { + ANTEATER_API_KEY: env.ANTEATER_API_KEY, AA_MONGODB_URI: env.MONGODB_URI_PROD, MAPBOX_ACCESS_TOKEN: env.MAPBOX_ACCESS_TOKEN ?? '', STAGE: env.NODE_ENV ?? 'development', From 13ff3d5597779c465834f8e1d6e4c0fa725655b9 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:16:55 -0800 Subject: [PATCH 15/34] chore: undo unnecessary changes to backend --- apps/backend/scripts/build.mjs | 22 +++++++++++----------- apps/backend/src/db/ddb.ts | 1 + apps/backend/src/index.ts | 10 ++++------ apps/backend/src/lambda.ts | 2 +- apps/backend/src/routers/users.ts | 2 +- apps/backend/src/routers/zotcours.ts | 18 ++++++++++-------- 6 files changed, 28 insertions(+), 27 deletions(-) diff --git a/apps/backend/scripts/build.mjs b/apps/backend/scripts/build.mjs index 46c619122..5a59ecd3d 100644 --- a/apps/backend/scripts/build.mjs +++ b/apps/backend/scripts/build.mjs @@ -1,15 +1,15 @@ -import { build } from 'esbuild'; +import { build } from 'esbuild' async function main() { - await build({ - bundle: true, - minify: true, - platform: 'node', - outdir: 'dist', - entryPoints: { - lambda: 'src/lambda.ts', - }, - }); + await build({ + bundle: true, + minify: true, + platform: 'node', + outdir: 'dist', + entryPoints: { + lambda: 'src/lambda.ts', + } + }) } -main(); +main() diff --git a/apps/backend/src/db/ddb.ts b/apps/backend/src/db/ddb.ts index 62a8ebc41..b26f6e72a 100644 --- a/apps/backend/src/db/ddb.ts +++ b/apps/backend/src/db/ddb.ts @@ -171,6 +171,7 @@ class DDBClient>> { } else { return parsedUserData.data; } + } } diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 065c1dc94..bb1e2a356 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -33,18 +33,16 @@ export async function start(corsEnabled = false) { app.use('/mapbox/tiles/*', async (req, res) => { const searchParams = new URLSearchParams(req.query as any); searchParams.set('access_token', env.MAPBOX_ACCESS_TOKEN); - const url = `${MAPBOX_API_URL}/styles/v1/mapbox/streets-v11/tiles/${ - (req.params as any)[0] - }?${searchParams.toString()}`; + const url = `${MAPBOX_API_URL}/styles/v1/mapbox/streets-v11/tiles/${(req.params as any)[0]}?${searchParams.toString()}`; const buffer = await fetch(url).then((res) => res.arrayBuffer()); - res.type('image/png'); - res.send(Buffer.from(buffer)); + res.type('image/png') + res.send(Buffer.from(buffer)) // // res.header('Content-Security-Policy', "img-src 'self'"); // https://stackoverflow.com/questions/56386307/loading-of-a-resource-blocked-by-content-security-policy // // res.header('Access-Control-Allow-Methods', 'GET, OPTIONS') // res.type('image/png') // res.send(result) }); - + app.use( '/trpc', createExpressMiddleware({ diff --git a/apps/backend/src/lambda.ts b/apps/backend/src/lambda.ts index 05609fd91..7df5df2f9 100644 --- a/apps/backend/src/lambda.ts +++ b/apps/backend/src/lambda.ts @@ -1,8 +1,8 @@ import serverlessExpress from '@vendia/serverless-express'; import type { Context, Handler } from 'aws-lambda'; -import env from './env'; import { start } from '.'; import connectToMongoDB from '$db/mongodb'; +import env from './env'; let cachedHandler: Handler; diff --git a/apps/backend/src/routers/users.ts b/apps/backend/src/routers/users.ts index b15c6b9d1..82d09c5c3 100644 --- a/apps/backend/src/routers/users.ts +++ b/apps/backend/src/routers/users.ts @@ -1,8 +1,8 @@ import { type } from 'arktype'; import { UserSchema } from '@packages/antalmanac-types'; -import { TRPCError } from '@trpc/server'; import { router, procedure } from '../trpc'; import { ddbClient, VISIBILITY } from '../db/ddb'; +import { TRPCError } from '@trpc/server'; const userInputSchema = type([{ userId: 'string' }, '|', { googleId: 'string' }]); diff --git a/apps/backend/src/routers/zotcours.ts b/apps/backend/src/routers/zotcours.ts index 31f8f668a..9ce5b1dae 100644 --- a/apps/backend/src/routers/zotcours.ts +++ b/apps/backend/src/routers/zotcours.ts @@ -1,15 +1,17 @@ -import { type } from 'arktype'; import { procedure, router } from '../trpc'; +import {type} from 'arktype'; const zotcourseUrl = 'https://zotcourse.appspot.com/schedule/load'; const zotcourseRouter = router({ - getUserData: procedure.input(type({ scheduleName: 'string' }).assert).mutation(async ({ input }) => { - const url = new URL(zotcourseUrl); - url.searchParams.append('username', input.scheduleName); - const response = await fetch(url); - return await response.json(); - }), + getUserData: procedure + .input(type({ scheduleName: 'string' }).assert) + .mutation(async ({ input }) => { + let url = new URL(zotcourseUrl); + url.searchParams.append('username', input.scheduleName); + const response = await fetch(url); + return await response.json(); + }), }); -export default zotcourseRouter; +export default zotcourseRouter; \ No newline at end of file From 58a960344201e9599309baf27c3e0b2e95fc8aba Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Tue, 12 Nov 2024 11:25:00 -0800 Subject: [PATCH 16/34] fix: tests --- apps/antalmanac/tests/calendarize-helpers.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/antalmanac/tests/calendarize-helpers.test.ts b/apps/antalmanac/tests/calendarize-helpers.test.ts index 13ae4b2dd..3855a897d 100644 --- a/apps/antalmanac/tests/calendarize-helpers.test.ts +++ b/apps/antalmanac/tests/calendarize-helpers.test.ts @@ -77,7 +77,7 @@ describe('calendarize-helpers', () => { courseTitle: 'placeholderCourseTitle', instructors: [], sectionCode: 'placeholderSectionCode', - sectionType: 'placeholderSectionType', + sectionType: 'Lec', start: new Date(2018, 0, 1, 1, 2), end: new Date(2018, 0, 1, 3, 4), finalExam: { @@ -106,7 +106,7 @@ describe('calendarize-helpers', () => { courseTitle: 'placeholderCourseTitle', instructors: [], sectionCode: 'placeholderSectionCode', - sectionType: 'placeholderSectionType', + sectionType: 'Lec', start: new Date(2018, 0, 3, 1, 2), end: new Date(2018, 0, 3, 3, 4), finalExam: { @@ -135,7 +135,7 @@ describe('calendarize-helpers', () => { courseTitle: 'placeholderCourseTitle', instructors: [], sectionCode: 'placeholderSectionCode', - sectionType: 'placeholderSectionType', + sectionType: 'Lec', start: new Date(2018, 0, 5, 1, 2), end: new Date(2018, 0, 5, 3, 4), finalExam: { From 9b4f174b1eae85c2fe66d1bfec7def60cf645d60 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:41:07 -0800 Subject: [PATCH 17/34] feat: remove hardcoded prereq tree types --- apps/backend/src/env.ts | 1 + packages/anteater-api-types/src/courses.ts | 35 +++------------------- 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/apps/backend/src/env.ts b/apps/backend/src/env.ts index 407dc2f66..32bcd6cd6 100644 --- a/apps/backend/src/env.ts +++ b/apps/backend/src/env.ts @@ -8,6 +8,7 @@ const Environment = type({ AWS_REGION: 'string', MAPBOX_ACCESS_TOKEN: 'string', 'PR_NUM?': 'number', + STAGE: "'prod' | 'dev' | 'local'", }); const env = Environment.assert({ ...process.env }); diff --git a/packages/anteater-api-types/src/courses.ts b/packages/anteater-api-types/src/courses.ts index 56040cb12..86533cb36 100644 --- a/packages/anteater-api-types/src/courses.ts +++ b/packages/anteater-api-types/src/courses.ts @@ -1,34 +1,7 @@ -import { paths } from './generated/anteater-api-types'; +import { components, paths } from './generated/anteater-api-types'; -type _Course = paths['/v2/rest/courses/{id}']['get']['responses'][200]['content']['application/json']['data']; +export type Course = paths['/v2/rest/courses/{id}']['get']['responses'][200]['content']['application/json']['data']; -type CoursePrerequisite = { - prereqType: 'course'; - coreq: false; - courseId: string; - minGrade?: string; -}; +export type Prerequisite = components['schemas']['prereq']; -type CourseCorequisite = { - prereqType: 'course'; - coreq: true; - courseId: string; -}; - -type ExamPrerequisite = { - prereqType: 'exam'; - examName: string; - minGrade?: string; -}; - -export type Prerequisite = CoursePrerequisite | CourseCorequisite | ExamPrerequisite; - -export type PrerequisiteTree = { - AND?: Array; - OR?: Array; - NOT?: Array; -}; - -export interface Course extends _Course { - prerequisiteTree: PrerequisiteTree; -} +export type PrerequisiteTree = components['schemas']['prereqTree']; From a3a9a8a2ba8da0364b86690f3c2d12929445d09a Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Sat, 16 Nov 2024 17:22:45 -0800 Subject: [PATCH 18/34] feat: hybrid fuzzy search --- .../CoursePane/SearchForm/FuzzySearch.tsx | 84 ++++---- apps/backend/package.json | 1 + apps/backend/src/routers/index.ts | 2 + apps/backend/src/routers/search.ts | 89 +++++++++ apps/backend/src/searchData.ts | 187 ++++++++++++++++++ packages/anteater-api-types/src/index.ts | 1 + packages/anteater-api-types/src/search.ts | 24 +++ packages/types/src/index.ts | 1 + pnpm-lock.yaml | 8 + 9 files changed, 349 insertions(+), 48 deletions(-) create mode 100644 apps/backend/src/routers/search.ts create mode 100644 apps/backend/src/searchData.ts create mode 100644 packages/anteater-api-types/src/search.ts diff --git a/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/FuzzySearch.tsx b/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/FuzzySearch.tsx index ceb799f20..d55eaac67 100644 --- a/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/FuzzySearch.tsx +++ b/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/FuzzySearch.tsx @@ -1,20 +1,20 @@ import TextField from '@material-ui/core/TextField'; import Autocomplete, { AutocompleteInputChangeReason } from '@material-ui/lab/Autocomplete'; +import type { SearchResult } from '@packages/antalmanac-types'; import { PureComponent } from 'react'; import UAParser from 'ua-parser-js'; -import search from 'websoc-fuzzy-search'; - -type SearchResult = ReturnType; import RightPaneStore from '../../RightPaneStore'; import analyticsEnum, { logAnalytics } from '$lib/analytics'; +import trpc from '$lib/api/trpc'; + +const SEARCH_TIMEOUT_MS = 300; const emojiMap: Record = { GE_CATEGORY: '🏫', // U+1F3EB :school: DEPARTMENT: '🏢', // U+1F3E2 :office: COURSE: '📚', // U+1F4DA :books: - INSTRUCTOR: '🍎', // U+1F34E :apple: }; const romanArr = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII']; @@ -34,10 +34,12 @@ interface FuzzySearchProps { } interface FuzzySearchState { - cache: Record; + cache: Record | undefined>; open: boolean; - results: SearchResult; + results: Record | undefined; value: string; + loading: boolean; + pendingRequest?: number; } class FuzzySearch extends PureComponent { @@ -46,12 +48,14 @@ class FuzzySearch extends PureComponent { open: false, results: {}, value: '', + loading: false, + pendingRequest: undefined, }; doSearch = (value: string) => { if (!value) return; const emoji = value.slice(0, 2); - const ident: string[] = emoji === emojiMap.INSTRUCTOR ? [value.slice(3)] : value.slice(3).split(':'); + const ident = value.slice(3).split(':'); const term = RightPaneStore.getFormData().term; RightPaneStore.resetFormValues(); RightPaneStore.updateFormValue('term', term); @@ -68,36 +72,11 @@ class FuzzySearch extends PureComponent { break; case emojiMap.COURSE: { const deptValue = ident[0].split(' ').slice(0, -1).join(' '); - let deptLabel; - for (const [key, value] of Object.entries(this.state.cache)) { - if (Object.keys(value ?? {}).includes(deptValue)) { - deptLabel = this.state.cache[key]?.[deptValue].name; - break; - } - } - if (!deptLabel) { - const deptSearch = search({ query: deptValue.toLowerCase(), numResults: 1 }); - if (deptSearch?.[deptValue]) { - deptLabel = deptSearch[deptValue].name; - this.setState({ - cache: { - ...this.state.cache, - [deptValue.toLowerCase()]: deptSearch, - }, - }); - } - } RightPaneStore.updateFormValue('deptValue', deptValue); - RightPaneStore.updateFormValue('deptLabel', `${deptValue}: ${deptLabel}`); + RightPaneStore.updateFormValue('deptLabel', deptValue); RightPaneStore.updateFormValue('courseNumber', ident[0].split(' ').slice(-1)[0]); break; } - case emojiMap.INSTRUCTOR: - RightPaneStore.updateFormValue( - 'instructor', - Object.keys(this.state.results ?? {}).filter((x) => this.state.results?.[x].name === ident[0])[0] - ); - break; default: break; } @@ -124,14 +103,10 @@ class FuzzySearch extends PureComponent { case 'DEPARTMENT': return `${emojiMap.DEPARTMENT} ${option}: ${object.name}`; case 'COURSE': - // @ts-expect-error type SearchResult.metadata can only be of type CourseMetaData in this case, but the type is not exposed so we can't cast directly return `${emojiMap.COURSE} ${object.metadata.department} ${object.metadata.number}: ${object.name}`; - case 'INSTRUCTOR': - return `${emojiMap.INSTRUCTOR} ${object.name}`; default: - break; + return ''; } - return ''; }; getOptionSelected = () => true; @@ -149,16 +124,28 @@ class FuzzySearch extends PureComponent { if (this.state.cache[this.state.value]) { this.setState({ results: this.state.cache[this.state.value] }); } else { - try { - const result = search({ query: this.state.value, numResults: 10 }); - this.setState({ - cache: { ...this.state.cache, [this.state.value]: result }, - results: result, - }); - } catch (e) { - this.setState({ results: {} }); - console.error(e); - } + this.setState({ results: {}, loading: true }, () => { + window.clearTimeout(this.state.pendingRequest); + const pendingRequest = window.setTimeout( + () => + trpc.search.doSearch + .query({ query: this.state.value }) + .then((result) => + this.setState({ + cache: { ...this.state.cache, [this.state.value]: result }, + results: result, + loading: false, + pendingRequest: undefined, + }) + ) + .catch((e) => { + this.setState({ results: {}, loading: false }); + console.error(e); + }), + SEARCH_TIMEOUT_MS + ); + this.setState({ pendingRequest }); + }); } } ); @@ -176,6 +163,7 @@ class FuzzySearch extends PureComponent { render() { return ( ( diff --git a/apps/backend/package.json b/apps/backend/package.json index 85c0f03b4..b62538090 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -10,6 +10,7 @@ "lint": "eslint --fix src" }, "dependencies": { + "@leeoniya/ufuzzy": "1.0.14", "@packages/antalmanac-types": "workspace:*", "@trpc/server": "^10.30.0", "@vendia/serverless-express": "^4.10.1", diff --git a/apps/backend/src/routers/index.ts b/apps/backend/src/routers/index.ts index 4545664fe..02bfbe834 100644 --- a/apps/backend/src/routers/index.ts +++ b/apps/backend/src/routers/index.ts @@ -6,12 +6,14 @@ import courseRouter from './course'; import websocRouter from './websoc'; import gradesRouter from './grades'; import enrollHistRouter from './enrollHist'; +import searchRouter from "./search"; const appRouter = router({ course: courseRouter, enrollHist: enrollHistRouter, grades: gradesRouter, news: newsRouter, + search: searchRouter, users: usersRouter, websoc: websocRouter, zotcourse: zotcourseRouter, diff --git a/apps/backend/src/routers/search.ts b/apps/backend/src/routers/search.ts new file mode 100644 index 000000000..593ecf1c3 --- /dev/null +++ b/apps/backend/src/routers/search.ts @@ -0,0 +1,89 @@ +import { z } from 'zod'; +import type { GESearchResult, SearchResponse, SearchResult } from '@packages/antalmanac-types'; +import { procedure, router } from '../trpc'; +import { DepartmentAliasKey, departmentAliasKeys, DepartmentKey, departmentKeys, toDepartment } from '../searchData'; +import uFuzzy from '@leeoniya/ufuzzy'; + +const geCategoryKeys = ['ge1a', 'ge1b', 'ge2', 'ge3', 'ge4', 'ge5a', 'ge5b', 'ge6', 'ge7', 'ge8'] as const; + +type GECategoryKey = (typeof geCategoryKeys)[number]; + +const geCategories: Record = { + ge1a: { type: 'GE_CATEGORY', name: 'Lower Division Writing' }, + ge1b: { type: 'GE_CATEGORY', name: 'Upper Division Writing' }, + ge2: { type: 'GE_CATEGORY', name: 'Science and Technology' }, + ge3: { type: 'GE_CATEGORY', name: 'Social and Behavioral Sciences' }, + ge4: { type: 'GE_CATEGORY', name: 'Arts and Humanities' }, + ge5a: { type: 'GE_CATEGORY', name: 'Quantitative Literacy' }, + ge5b: { type: 'GE_CATEGORY', name: 'Formal Reasoning' }, + ge6: { type: 'GE_CATEGORY', name: 'Language other than English' }, + ge7: { type: 'GE_CATEGORY', name: 'Multicultural Studies' }, + ge8: { type: 'GE_CATEGORY', name: 'International/Global Issues' }, +}; + +const toGESearchResult = (key: GECategoryKey): [string, SearchResult] => [ + key.toUpperCase().replace('GE', 'GE-'), + geCategories[key], +]; + +const queryFuzzySaas = async (query: string, take: number): Promise<[string, SearchResult][]> => + take < 1 + ? [] + : await fetch( + `https://anteaterapi.com/v2/rest/search?query=${encodeURIComponent( + query + )}&resultType=course&take=${take}`, + { + headers: { + ...(process.env.ANTEATER_API_KEY && { + Authorization: `Bearer ${process.env.ANTEATER_API_KEY}`, + }), + }, + } + ) + .then((r) => r.json()) + .then((r: SearchResponse) => + r.data.results + .filter((x) => x.type === 'course') + .map((x) => x.result) + .map((x) => [ + x.id, + { + type: 'COURSE' as const, + name: x.title, + metadata: { department: x.department, number: x.courseNumber }, + }, + ]) + ); + +const toMutable = (arr: readonly T[]): T[] => arr as T[]; + +const searchRouter = router({ + doSearch: procedure + .input(z.object({ query: z.string() })) + .query(async ({ input }): Promise> => { + const { query } = input; + const u = new uFuzzy(); + const matchedGEs = u.search(toMutable(geCategoryKeys), query)[0]?.map((i) => geCategoryKeys[i]) ?? []; + if (matchedGEs.length) return Object.fromEntries(matchedGEs.map(toGESearchResult)); + // TODO implement department searching + const matchedDeptAliases = ( + u.search(toMutable(departmentAliasKeys), query)[0]?.map((i) => departmentAliasKeys[i]) ?? [] + ).slice(0, 10); + const matchedDepts = + matchedDeptAliases.length === 10 + ? [] + : (u.search(toMutable(departmentKeys), query)[0]?.map((i) => departmentKeys[i]) ?? []).slice( + 0, + 10 - matchedDeptAliases.length + ); + return Object.fromEntries( + (matchedDeptAliases as Array) + .concat(matchedDepts as Array) + .map(toDepartment) + .concat(await queryFuzzySaas(input.query, 10 - matchedDepts.length)) + ); + }), +}); + +export default searchRouter; diff --git a/apps/backend/src/searchData.ts b/apps/backend/src/searchData.ts new file mode 100644 index 000000000..3fc86f7de --- /dev/null +++ b/apps/backend/src/searchData.ts @@ -0,0 +1,187 @@ +import type {DepartmentSearchResult, SearchResult} from '@packages/antalmanac-types'; + +// TODO implement codegen for this at CI time + +export const departmentKeys = ["ac eng","afam","anatomy","anthro","arabic","armn","art","art his","arts","asianam","bana","bats","biochem","bio sci","bme","cbe","cbems","chc/lat","chem","chinese","classic","clt&thy","cogs","com lit","compsci","critism","crm/law","cse","dance","data","dev bio","drama","earthss","eas","e asian","eco evo","econ","ecps","educ","eecs","ehs","english","engr","engrcee","engrmae","engrmse","epidem","euro st","fin","flm&mda","french","gdim","gen&sex","german","glblclt","glbl me","greek","hebrew","history","human","i&c sci","in4matx","inno","intl st","iran","italian","japanse","korean","latin","linguis","lit jrn","lps","lsci","math","med hum","mgmt","mgmt ep","mgmt fe","mgmt hc","mgmtmba","mgmtphd","m&mg","mol bio","mpac","mse","music","net sys","neurbio","nur sci","path","ped gen","persian","pharm","philos","phmd","phrmsci","phy sci","physics","physio","pol sci","portug","pp&d","psci","psy beh","psych","pubhlth","pub pol","rel std","rotc","russian","socecol","sociol","soc sci","spanish","spps","stats","swe","tox","ucdc","uni aff","uni stu","uppp","vietmse","vis std","womn st","writing"] as const; + +export type DepartmentKey = (typeof departmentKeys)[number]; + +export const departmentAliasKeys = ["aceng","arthis","biosci","chclat","cltthy","comlit","crmlaw","cs","devbio","easian","ecoevo","ess","eurost","flmmda","gensex","glblme","ics","inf","intlst","litjrn","medhum","mgmtep","mgmtfe","mgmthc","mmg","molbio","netsys","nursci","pedgen","physci","polsci","ppd","psybeh","pubpol","relstd","socsci","uniaff","unistu","visstd","womnst","wr"] as const; + +export type DepartmentAliasKey = (typeof departmentAliasKeys)[number]; + +export const departmentAliases: Record = { + aceng: 'ac eng', + arthis: 'art his', + biosci: 'bio sci', + chclat: 'chc/lat', + cltthy: 'clt&thy', + comlit: 'com lit', + crmlaw: 'crm/law', + cs: 'compsci', + devbio: 'dev bio', + easian: 'e asian', + ecoevo: 'eco evo', + ess: 'earthss', + eurost: 'euro st', + flmmda: 'flm&mda', + gensex: 'gen&sex', + glblme: 'glbl me', + ics: 'i&c sci', + inf: 'in4matx', + intlst: 'intl st', + litjrn: 'lit jrn', + medhum: 'med hum', + mgmtep: 'mgmt ep', + mgmtfe: 'mgmt fe', + mgmthc: 'mgmt hc', + mmg: 'm&mg', + molbio: 'mol bio', + netsys: 'net sys', + nursci: 'nur sci', + pedgen: 'ped gen', + physci: 'phy sci', + polsci: 'pol sci', + ppd: 'pp&d', + psybeh: 'psy beh', + pubpol: 'pub pol', + relstd: 'rel std', + socsci: 'soc sci', + uniaff: 'uni aff', + unistu: 'uni stu', + visstd: 'vis std', + womnst: 'womn st', + wr: 'writing', +}; + +const departments: Record = { + 'ac eng': { type: 'DEPARTMENT', name: 'Academic English' }, + afam: { type: 'DEPARTMENT', name: 'African American Studies' }, + anatomy: { type: 'DEPARTMENT', name: 'Anatomy and Neurobiology' }, + anthro: { type: 'DEPARTMENT', name: 'Anthropology' }, + arabic: { type: 'DEPARTMENT', name: 'Arabic' }, + armn: { type: 'DEPARTMENT', name: 'Armenian' }, + art: { type: 'DEPARTMENT', name: 'Art' }, + 'art his': { type: 'DEPARTMENT', name: 'Art History' }, + arts: { type: 'DEPARTMENT', name: 'Arts' }, + asianam: { type: 'DEPARTMENT', name: 'Asian American Studies' }, + bana: { type: 'DEPARTMENT', name: 'Business Analytics' }, + bats: { type: 'DEPARTMENT', name: 'Biomedical and Translational Science' }, + biochem: { type: 'DEPARTMENT', name: 'Biological Chemistry' }, + 'bio sci': { type: 'DEPARTMENT', name: 'Biological Sciences' }, + bme: { type: 'DEPARTMENT', name: 'Biomedical Engineering' }, + cbe: { type: 'DEPARTMENT', name: 'Chemical and Biomolecular Engineering' }, + cbems: { type: 'DEPARTMENT', name: 'Chemical Engineering and Materials Science' }, + 'chc/lat': { type: 'DEPARTMENT', name: 'Chicano/Latino Studies' }, + chem: { type: 'DEPARTMENT', name: 'Chemistry' }, + chinese: { type: 'DEPARTMENT', name: 'Chinese' }, + classic: { type: 'DEPARTMENT', name: 'Classics' }, + 'clt&thy': { type: 'DEPARTMENT', name: 'Culture and Theory' }, + cogs: { type: 'DEPARTMENT', name: 'Cognitive Sciences' }, + 'com lit': { type: 'DEPARTMENT', name: 'Comparative Literature' }, + compsci: { type: 'DEPARTMENT', name: 'Computer Science' }, + critism: { type: 'DEPARTMENT', name: 'Criticism' }, + 'crm/law': { type: 'DEPARTMENT', name: 'Criminology, Law and Society' }, + cse: { type: 'DEPARTMENT', name: 'Computer Science and Engineering' }, + dance: { type: 'DEPARTMENT', name: 'Dance' }, + data: { type: 'DEPARTMENT', name: 'Data Science' }, + 'dev bio': { type: 'DEPARTMENT', name: 'Developmental and Cell Biology' }, + drama: { type: 'DEPARTMENT', name: 'Drama' }, + earthss: { type: 'DEPARTMENT', name: 'Earth System Science' }, + eas: { type: 'DEPARTMENT', name: 'East Asian Studies' }, + 'e asian': { type: 'DEPARTMENT', name: 'East Asian Languages and Literatures' }, + 'eco evo': { type: 'DEPARTMENT', name: 'Ecology and Evolutionary Biology' }, + econ: { type: 'DEPARTMENT', name: 'Economics' }, + ecps: { type: 'DEPARTMENT', name: 'Embedded and Cyber-Physical Systems' }, + educ: { type: 'DEPARTMENT', name: 'Education' }, + eecs: { type: 'DEPARTMENT', name: 'Electrical Engineering & Computer Science' }, + ehs: { type: 'DEPARTMENT', name: 'Environmental Health Sciences' }, + english: { type: 'DEPARTMENT', name: 'English' }, + engr: { type: 'DEPARTMENT', name: 'Engineering' }, + engrcee: { type: 'DEPARTMENT', name: 'Civil and Environmental Engineering' }, + engrmae: { type: 'DEPARTMENT', name: 'Mechanical and Aerospace Engineering' }, + engrmse: { type: 'DEPARTMENT', name: 'Materials Science and Engineering' }, + epidem: { type: 'DEPARTMENT', name: 'Epidemiology' }, + 'euro st': { type: 'DEPARTMENT', name: 'European Studies' }, + fin: { type: 'DEPARTMENT', name: 'Finance' }, + 'flm&mda': { type: 'DEPARTMENT', name: 'Film and Media Studies' }, + french: { type: 'DEPARTMENT', name: 'French' }, + gdim: { type: 'DEPARTMENT', name: 'Game Design and Interactive Media' }, + 'gen&sex': { type: 'DEPARTMENT', name: 'Gender and Sexuality Studies' }, + german: { type: 'DEPARTMENT', name: 'German' }, + glblclt: { type: 'DEPARTMENT', name: 'Global Cultures' }, + 'glbl me': { type: 'DEPARTMENT', name: 'Global Middle East Studies' }, + greek: { type: 'DEPARTMENT', name: 'Greek' }, + hebrew: { type: 'DEPARTMENT', name: 'Hebrew' }, + history: { type: 'DEPARTMENT', name: 'History' }, + human: { type: 'DEPARTMENT', name: 'Humanities' }, + 'i&c sci': { type: 'DEPARTMENT', name: 'Information and Computer Science' }, + in4matx: { type: 'DEPARTMENT', name: 'Informatics' }, + inno: { type: 'DEPARTMENT', name: 'Innovation and Entrepreneurship' }, + 'intl st': { type: 'DEPARTMENT', name: 'International Studies' }, + iran: { type: 'DEPARTMENT', name: 'Iranian Studies' }, + italian: { type: 'DEPARTMENT', name: 'Italian' }, + japanse: { type: 'DEPARTMENT', name: 'Japanese' }, + korean: { type: 'DEPARTMENT', name: 'Korean' }, + latin: { type: 'DEPARTMENT', name: 'Latin' }, + linguis: { type: 'DEPARTMENT', name: 'Linguistics' }, + 'lit jrn': { type: 'DEPARTMENT', name: 'Literary Journalism' }, + lps: { type: 'DEPARTMENT', name: 'Logic and Philosophy of Science' }, + lsci: { type: 'DEPARTMENT', name: 'Language Science' }, + math: { type: 'DEPARTMENT', name: 'Mathematics' }, + 'med hum': { type: 'DEPARTMENT', name: 'Medical Humanities' }, + mgmt: { type: 'DEPARTMENT', name: 'Management' }, + 'mgmt ep': { type: 'DEPARTMENT', name: 'Executive MBA' }, + 'mgmt fe': { type: 'DEPARTMENT', name: 'Fully Employed MBA' }, + 'mgmt hc': { type: 'DEPARTMENT', name: 'Health Care MBA' }, + mgmtmba: { type: 'DEPARTMENT', name: 'Management MBA' }, + mgmtphd: { type: 'DEPARTMENT', name: 'Management PhD' }, + 'm&mg': { type: 'DEPARTMENT', name: 'Microbiology and Molecular Genetics' }, + 'mol bio': { type: 'DEPARTMENT', name: 'Molecular Biology and Biochemistry' }, + mpac: { type: 'DEPARTMENT', name: 'Master of Professional Accountancy' }, + mse: { type: 'DEPARTMENT', name: 'Materials Science and Engineering' }, + music: { type: 'DEPARTMENT', name: 'Music' }, + 'net sys': { type: 'DEPARTMENT', name: 'Networked Systems' }, + neurbio: { type: 'DEPARTMENT', name: 'Neurobiology and Behavior' }, + 'nur sci': { type: 'DEPARTMENT', name: 'Nursing Science' }, + path: { type: 'DEPARTMENT', name: 'Pathology and Laboratory Medicine' }, + 'ped gen': { type: 'DEPARTMENT', name: 'Pediatrics Genetics' }, + persian: { type: 'DEPARTMENT', name: 'Persian' }, + pharm: { type: 'DEPARTMENT', name: 'Medical Pharmacology' }, + philos: { type: 'DEPARTMENT', name: 'Philosophy' }, + phmd: { type: 'DEPARTMENT', name: 'Pharmacy' }, + phrmsci: { type: 'DEPARTMENT', name: 'Pharmaceutical Sciences' }, + 'phy sci': { type: 'DEPARTMENT', name: 'Physical Science' }, + physics: { type: 'DEPARTMENT', name: 'Physics' }, + physio: { type: 'DEPARTMENT', name: 'Physiology and Biophysics' }, + 'pol sci': { type: 'DEPARTMENT', name: 'Political Science' }, + portug: { type: 'DEPARTMENT', name: 'Portuguese' }, + 'pp&d': { type: 'DEPARTMENT', name: 'Planning, Policy, and Design' }, + psci: { type: 'DEPARTMENT', name: 'Psychological Science' }, + 'psy beh': { type: 'DEPARTMENT', name: 'Psychology and Social Behavior' }, + psych: { type: 'DEPARTMENT', name: 'Psychology' }, + pubhlth: { type: 'DEPARTMENT', name: 'Public Health' }, + 'pub pol': { type: 'DEPARTMENT', name: 'Public Policy' }, + 'rel std': { type: 'DEPARTMENT', name: 'Religious Studies' }, + rotc: { type: 'DEPARTMENT', name: "Reserve Officers' Training Corps" }, + russian: { type: 'DEPARTMENT', name: 'Russian' }, + socecol: { type: 'DEPARTMENT', name: 'Social Ecology' }, + sociol: { type: 'DEPARTMENT', name: 'Sociology' }, + 'soc sci': { type: 'DEPARTMENT', name: 'Social Science' }, + spanish: { type: 'DEPARTMENT', name: 'Spanish' }, + spps: { type: 'DEPARTMENT', name: 'Social Policy and Public Service' }, + stats: { type: 'DEPARTMENT', name: 'Statistics' }, + swe: { type: 'DEPARTMENT', name: 'Software Engineering' }, + tox: { type: 'DEPARTMENT', name: 'Toxicology' }, + ucdc: { type: 'DEPARTMENT', name: 'UC Washington DC' }, + 'uni aff': { type: 'DEPARTMENT', name: 'University Affairs' }, + 'uni stu': { type: 'DEPARTMENT', name: 'University Studies' }, + uppp: { type: 'DEPARTMENT', name: 'Urban Policy and Public Planning' }, + vietmse: { type: 'DEPARTMENT', name: 'Vietnamese' }, + 'vis std': { type: 'DEPARTMENT', name: 'Visual Studies' }, + 'womn st': { type: 'DEPARTMENT', name: "Women's Studies" }, + writing: { type: 'DEPARTMENT', name: 'Writing' }, +}; + +export const toDepartment = (key: DepartmentKey | DepartmentAliasKey): [string, SearchResult] => + [(departmentAliases[key as DepartmentAliasKey] ?? key).toUpperCase(), departments[departmentAliases[key as DepartmentAliasKey] ?? key]]; diff --git a/packages/anteater-api-types/src/index.ts b/packages/anteater-api-types/src/index.ts index 0e0f36f20..c6584dbad 100644 --- a/packages/anteater-api-types/src/index.ts +++ b/packages/anteater-api-types/src/index.ts @@ -1,4 +1,5 @@ export * from './courses'; export * from './enrollHist'; export * from './grades'; +export * from './search'; export * from './websoc'; diff --git a/packages/anteater-api-types/src/search.ts b/packages/anteater-api-types/src/search.ts new file mode 100644 index 000000000..917ffa0bc --- /dev/null +++ b/packages/anteater-api-types/src/search.ts @@ -0,0 +1,24 @@ +import { paths } from './generated/anteater-api-types'; + +export type GESearchResult = { + type: 'GE_CATEGORY'; + name: string; +}; + +export type DepartmentSearchResult = { + type: 'DEPARTMENT'; + name: string; +}; + +export type CourseSearchResult = { + type: 'COURSE'; + name: string; + metadata: { + department: string; + number: string; + }; +}; + +export type SearchResult = GESearchResult | DepartmentSearchResult | CourseSearchResult; + +export type SearchResponse = paths['/v2/rest/search']['get']['responses'][200]['content']['application/json']; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 2c594c8d0..b6fdee15e 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -7,4 +7,5 @@ export * from './websoc'; export * from '@packages/anteater-api-types/src/courses'; export * from '@packages/anteater-api-types/src/enrollHist'; export * from '@packages/anteater-api-types/src/grades'; +export * from '@packages/anteater-api-types/src/search'; export * from '@packages/anteater-api-types/src/websoc'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36eb8d70f..9362aea5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -291,6 +291,9 @@ importers: apps/backend: dependencies: + '@leeoniya/ufuzzy': + specifier: 1.0.14 + version: 1.0.14 '@packages/antalmanac-types': specifier: workspace:* version: link:../../packages/types @@ -1482,6 +1485,9 @@ packages: '@kurkle/color@0.3.2': resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==} + '@leeoniya/ufuzzy@1.0.14': + resolution: {integrity: sha512-/xF4baYuCQMo+L/fMSUrZnibcu0BquEGnbxfVPiZhs/NbJeKj4c/UmFpQzW9Us0w45ui/yYW3vyaqawhNYsTzA==} + '@mapbox/corslite@0.0.7': resolution: {integrity: sha512-w/uS474VFjmqQ7fFWIMZINQM1BAQxDLuoJaZZIPES1BmeYpCtlh9MtbFxKGGDAsfvut8/HircIsVvEYRjQ+iMg==} @@ -6662,6 +6668,8 @@ snapshots: '@kurkle/color@0.3.2': {} + '@leeoniya/ufuzzy@1.0.14': {} + '@mapbox/corslite@0.0.7': {} '@mapbox/polyline@0.2.0': {} From 3c2578299b6b8dbddb7e842d3581cbaf9f714ac0 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Sun, 17 Nov 2024 14:47:15 -0800 Subject: [PATCH 19/34] fix(backend): remove constraint on process.env.STAGE --- apps/backend/src/env.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/backend/src/env.ts b/apps/backend/src/env.ts index 32bcd6cd6..407dc2f66 100644 --- a/apps/backend/src/env.ts +++ b/apps/backend/src/env.ts @@ -8,7 +8,6 @@ const Environment = type({ AWS_REGION: 'string', MAPBOX_ACCESS_TOKEN: 'string', 'PR_NUM?': 'number', - STAGE: "'prod' | 'dev' | 'local'", }); const env = Environment.assert({ ...process.env }); From 40770e843f82f913867898c760122fc6b095f954 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Sun, 17 Nov 2024 15:21:58 -0800 Subject: [PATCH 20/34] feat: env shim but string, sort websoc resp --- apps/backend/src/env.ts | 1 + apps/backend/src/routers/websoc.ts | 41 ++++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/apps/backend/src/env.ts b/apps/backend/src/env.ts index 407dc2f66..feb3afceb 100644 --- a/apps/backend/src/env.ts +++ b/apps/backend/src/env.ts @@ -8,6 +8,7 @@ const Environment = type({ AWS_REGION: 'string', MAPBOX_ACCESS_TOKEN: 'string', 'PR_NUM?': 'number', + STAGE: "string", }); const env = Environment.assert({ ...process.env }); diff --git a/apps/backend/src/routers/websoc.ts b/apps/backend/src/routers/websoc.ts index c23c8df92..2bdfff3bd 100644 --- a/apps/backend/src/routers/websoc.ts +++ b/apps/backend/src/routers/websoc.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import type { WebsocAPIResponse, CourseInfo } from '@packages/antalmanac-types'; +import type {WebsocAPIResponse, CourseInfo, WebsocCourse} from '@packages/antalmanac-types'; import { procedure, router } from '../trpc'; function cleanSearchParams(record: Record) { @@ -24,6 +24,29 @@ function cleanSearchParams(record: Record) { return record; } +function compareCourses(a: WebsocCourse, b: WebsocCourse) { + const aNum = Number.parseInt(a.courseNumber.replaceAll(/\D/g, ''), 10); + const bNum = Number.parseInt(b.courseNumber.replaceAll(/\D/g, ''), 10); + const diffSign = Math.sign(aNum - bNum); + return diffSign === 0 ? a.courseNumber.localeCompare(b.courseNumber) : diffSign; +} + +function sortWebsocResponse(response: WebsocAPIResponse) { + response.schools.sort((a, b) => a.schoolName.localeCompare(b.schoolName)); + for (const school of response.schools) { + school.departments.sort((a, b) => a.deptCode.localeCompare(b.deptCode)); + for (const department of school.departments) { + department.courses.sort(compareCourses); + for (const course of department.courses) { + course.sections.sort((a, b) => + Math.sign(Number.parseInt(a.sectionCode, 10) - Number.parseInt(b.sectionCode, 10)) + ); + } + } + } + return response; +} + const queryWebSoc = async ({ input }: { input: Record }) => await fetch(`https://anteaterapi.com/v2/rest/websoc?${new URLSearchParams(cleanSearchParams(input))}`, { headers: { @@ -31,11 +54,11 @@ const queryWebSoc = async ({ input }: { input: Record }) => }, }) .then((data) => data.json()) - .then((data) => data.data as WebsocAPIResponse); + .then((data) => sortWebsocResponse(data.data as WebsocAPIResponse)); -function combineSOCObjects(SOCObjects: WebsocAPIResponse[]) { - const combined = SOCObjects.shift() as WebsocAPIResponse; - for (const res of SOCObjects) { +function combineWebsocResponses(responses: WebsocAPIResponse[]) { + const combined: WebsocAPIResponse = { schools: [] }; + for (const res of responses) { for (const school of res.schools) { const schoolIndex = combined.schools.findIndex((s) => s.schoolName === school.schoolName); if (schoolIndex !== -1) { @@ -49,11 +72,7 @@ function combineSOCObjects(SOCObjects: WebsocAPIResponse[]) { courses.add(course); } const coursesArray = Array.from(courses); - coursesArray.sort( - (left, right) => - parseInt(left.courseNumber.replace(/\D/g, '')) - - parseInt(right.courseNumber.replace(/\D/g, '')) - ); + coursesArray.sort(compareCourses); combined.schools[schoolIndex].departments[deptIndex].courses = coursesArray; } else { combined.schools[schoolIndex].departments.push(dept); @@ -78,7 +97,7 @@ const websocRouter = router({ req[input.fieldName] = field; responses.push(await queryWebSoc({ input: req })); } - return combineSOCObjects(responses); + return combineWebsocResponses(responses); }), getCourseInfo: procedure.input(z.record(z.string(), z.string())).query(async ({ input }) => { const res = await queryWebSoc({ input }); From f7795a07bf9717c951ff517555275949a3255e61 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Sun, 17 Nov 2024 16:48:30 -0800 Subject: [PATCH 21/34] feat: codegen'd local fuzzy search --- apps/backend/.gitignore | 1 + apps/backend/package.json | 6 +- apps/backend/scripts/get-search-data.ts | 60 ++++++++++++++++++ apps/backend/src/routers/search.ts | 62 +++++-------------- apps/backend/tsconfig.json | 2 +- packages/anteater-api-types/src/index.ts | 1 - packages/types/src/index.ts | 2 +- .../src/search.ts | 6 +- pnpm-lock.yaml | 8 +++ 9 files changed, 92 insertions(+), 56 deletions(-) create mode 100644 apps/backend/.gitignore create mode 100644 apps/backend/scripts/get-search-data.ts rename packages/{anteater-api-types => types}/src/search.ts (70%) diff --git a/apps/backend/.gitignore b/apps/backend/.gitignore new file mode 100644 index 000000000..d3db8f9b1 --- /dev/null +++ b/apps/backend/.gitignore @@ -0,0 +1 @@ +src/generated/ diff --git a/apps/backend/package.json b/apps/backend/package.json index b62538090..ad37ebd9b 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -4,8 +4,11 @@ "description": "Backend for AntAlmanac", "scripts": { "dev": "tsx watch src/index.ts", + "get-search-data": "tsx scripts/get-search-data.ts", + "prebuild": "pnpm get-search-data", "build": "node scripts/build.mjs", - "start": "npm run dev", + "prestart": "pnpm get-search-data", + "start": "pnpm dev", "format": "prettier --write src", "lint": "eslint --fix src" }, @@ -18,6 +21,7 @@ "aws-lambda": "^1.0.7", "cors": "^2.8.5", "dotenv": "^16.0.3", + "fuzzysort": "3.1.0", "envalid": "^7.3.1", "express": "^4.18.2", "mongodb": "^5.0.1", diff --git a/apps/backend/scripts/get-search-data.ts b/apps/backend/scripts/get-search-data.ts new file mode 100644 index 000000000..457a2082b --- /dev/null +++ b/apps/backend/scripts/get-search-data.ts @@ -0,0 +1,60 @@ +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { mkdir, writeFile } from 'node:fs/promises'; +import {Course, CourseSearchResult, DepartmentSearchResult} from '@packages/antalmanac-types'; + +import "dotenv/config"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const MAX_COURSES = 10_000; + +const ALIASES: Record = { + "COMPSCI": "CS", + "EARTHSS": "ESS", + "I&C SCI": "ICS", + "IN4MATX": "INF", +} + +async function main() { + const apiKey = process.env.ANTEATER_API_KEY; + if (!apiKey) throw new Error("ANTEATER_API_KEY is required"); + const headers = { Authorization: `Bearer ${apiKey}` } + const courses: Course[] = []; + for (let skip = 0; skip < MAX_COURSES; skip += 100) { + await fetch(`https://anteaterapi.com/v2/rest/courses?take=100&skip=${skip}`, {headers}) + .then(x => x.json()) + .then(x => courses.push(...x.data as Course[])) + } + console.log(`Fetched ${courses.length} courses`); + const courseMap = new Map(); + const deptMap = new Map(); + for (const course of courses) { + courseMap.set(course.id, { + id: course.id, + type: "COURSE", + name: course.title, + alias: ALIASES[course.department], + metadata: { + department: course.department, + number: course.courseNumber, + } + }) + deptMap.set(course.department, { + id: course.department, + type: "DEPARTMENT", + name: course.departmentName, + alias: ALIASES[course.department] + }); + } + console.log(`Fetched ${deptMap.size} departments`); + await mkdir(join(__dirname, "../src/generated/"), { recursive: true }); + await writeFile(join(__dirname, "../src/generated/searchData.ts"), ` + import type { CourseSearchResult, DepartmentSearchResult } from "@packages/antalmanac-types"; + export const departments: Array = ${JSON.stringify(Array.from(deptMap.values()))}; + export const courses: Array = ${JSON.stringify(Array.from(courseMap.values()))}; + `) + console.log("All done.") +} + +main().then(); diff --git a/apps/backend/src/routers/search.ts b/apps/backend/src/routers/search.ts index 593ecf1c3..7acca8486 100644 --- a/apps/backend/src/routers/search.ts +++ b/apps/backend/src/routers/search.ts @@ -1,8 +1,9 @@ import { z } from 'zod'; -import type { GESearchResult, SearchResponse, SearchResult } from '@packages/antalmanac-types'; -import { procedure, router } from '../trpc'; -import { DepartmentAliasKey, departmentAliasKeys, DepartmentKey, departmentKeys, toDepartment } from '../searchData'; +import type { GESearchResult, SearchResult } from '@packages/antalmanac-types'; import uFuzzy from '@leeoniya/ufuzzy'; +import * as fuzzysort from "fuzzysort"; +import { procedure, router } from '../trpc'; +import {courses, departments} from "../generated/searchData"; const geCategoryKeys = ['ge1a', 'ge1b', 'ge2', 'ge3', 'ge4', 'ge5a', 'ge5b', 'ge6', 'ge7', 'ge8'] as const; @@ -26,36 +27,6 @@ const toGESearchResult = (key: GECategoryKey): [string, SearchResult] => [ geCategories[key], ]; -const queryFuzzySaas = async (query: string, take: number): Promise<[string, SearchResult][]> => - take < 1 - ? [] - : await fetch( - `https://anteaterapi.com/v2/rest/search?query=${encodeURIComponent( - query - )}&resultType=course&take=${take}`, - { - headers: { - ...(process.env.ANTEATER_API_KEY && { - Authorization: `Bearer ${process.env.ANTEATER_API_KEY}`, - }), - }, - } - ) - .then((r) => r.json()) - .then((r: SearchResponse) => - r.data.results - .filter((x) => x.type === 'course') - .map((x) => x.result) - .map((x) => [ - x.id, - { - type: 'COURSE' as const, - name: x.title, - metadata: { department: x.department, number: x.courseNumber }, - }, - ]) - ); - const toMutable = (arr: readonly T[]): T[] => arr as T[]; const searchRouter = router({ @@ -66,22 +37,17 @@ const searchRouter = router({ const u = new uFuzzy(); const matchedGEs = u.search(toMutable(geCategoryKeys), query)[0]?.map((i) => geCategoryKeys[i]) ?? []; if (matchedGEs.length) return Object.fromEntries(matchedGEs.map(toGESearchResult)); - // TODO implement department searching - const matchedDeptAliases = ( - u.search(toMutable(departmentAliasKeys), query)[0]?.map((i) => departmentAliasKeys[i]) ?? [] - ).slice(0, 10); - const matchedDepts = - matchedDeptAliases.length === 10 - ? [] - : (u.search(toMutable(departmentKeys), query)[0]?.map((i) => departmentKeys[i]) ?? []).slice( - 0, - 10 - matchedDeptAliases.length - ); + const matchedDepts = fuzzysort.go(query, departments, { + keys: ['id', 'alias'], + limit: 10 + }) + const matchedCourses = matchedDepts.length === 10 ? [] : fuzzysort.go(query, courses, { + keys: ['id', 'name', 'alias', 'metadata.department', 'metadata.number'], + limit: 10 - matchedDepts.length + }) return Object.fromEntries( - (matchedDeptAliases as Array) - .concat(matchedDepts as Array) - .map(toDepartment) - .concat(await queryFuzzySaas(input.query, 10 - matchedDepts.length)) + [...matchedDepts.map(x => [x.obj.id, x.obj]), + ...matchedCourses.map(x => [x.obj.id, x.obj]),] ); }), }); diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index 9c62a719b..b872dac8a 100644 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -16,6 +16,6 @@ "$db/*": ["./src/db/*"] } }, - "include": ["src/**/*"], + "include": ["src/**/*", "scripts/**/*.ts"], "exclude": ["node_modules", "dist"] } diff --git a/packages/anteater-api-types/src/index.ts b/packages/anteater-api-types/src/index.ts index c6584dbad..0e0f36f20 100644 --- a/packages/anteater-api-types/src/index.ts +++ b/packages/anteater-api-types/src/index.ts @@ -1,5 +1,4 @@ export * from './courses'; export * from './enrollHist'; export * from './grades'; -export * from './search'; export * from './websoc'; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index b6fdee15e..493f4d30f 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -3,9 +3,9 @@ export * from './customevent'; export * from './courseData'; export * from './user'; export * from './legacy'; +export * from './search'; export * from './websoc'; export * from '@packages/anteater-api-types/src/courses'; export * from '@packages/anteater-api-types/src/enrollHist'; export * from '@packages/anteater-api-types/src/grades'; -export * from '@packages/anteater-api-types/src/search'; export * from '@packages/anteater-api-types/src/websoc'; diff --git a/packages/anteater-api-types/src/search.ts b/packages/types/src/search.ts similarity index 70% rename from packages/anteater-api-types/src/search.ts rename to packages/types/src/search.ts index 917ffa0bc..a74dab384 100644 --- a/packages/anteater-api-types/src/search.ts +++ b/packages/types/src/search.ts @@ -1,5 +1,3 @@ -import { paths } from './generated/anteater-api-types'; - export type GESearchResult = { type: 'GE_CATEGORY'; name: string; @@ -8,11 +6,13 @@ export type GESearchResult = { export type DepartmentSearchResult = { type: 'DEPARTMENT'; name: string; + alias?: string; }; export type CourseSearchResult = { type: 'COURSE'; name: string; + alias?: string; metadata: { department: string; number: string; @@ -20,5 +20,3 @@ export type CourseSearchResult = { }; export type SearchResult = GESearchResult | DepartmentSearchResult | CourseSearchResult; - -export type SearchResponse = paths['/v2/rest/search']['get']['responses'][200]['content']['application/json']; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9362aea5c..3f19c8278 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -321,6 +321,9 @@ importers: express: specifier: ^4.18.2 version: 4.18.2 + fuzzysort: + specifier: 3.1.0 + version: 3.1.0 mongodb: specifier: ^5.0.1 version: 5.0.1 @@ -3260,6 +3263,9 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + fuzzysort@3.1.0: + resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -8941,6 +8947,8 @@ snapshots: functions-have-names@1.2.3: {} + fuzzysort@3.1.0: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} From 51e51b547df380c8a70132e34f17bacd16a8a554 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Sun, 17 Nov 2024 16:52:37 -0800 Subject: [PATCH 22/34] fix(backend): websoc tweaks --- apps/backend/src/routers/websoc.ts | 32 +++++++++++++++++++----------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/apps/backend/src/routers/websoc.ts b/apps/backend/src/routers/websoc.ts index 2bdfff3bd..ac8cdf6ad 100644 --- a/apps/backend/src/routers/websoc.ts +++ b/apps/backend/src/routers/websoc.ts @@ -2,28 +2,36 @@ import { z } from 'zod'; import type {WebsocAPIResponse, CourseInfo, WebsocCourse} from '@packages/antalmanac-types'; import { procedure, router } from '../trpc'; -function cleanSearchParams(record: Record) { - if ('term' in record) { - const termValue = record['term']; +function sanitizeSearchParams(params: Record) { + if ('term' in params) { + const termValue = params.quarter; const termParts = termValue.split(' '); if (termParts.length === 2) { const [year, quarter] = termParts; - delete record['term']; - record['quarter'] = quarter; - record['year'] = year; + delete params.term; + params.quarter = quarter; + params.quarter = year; } } - if ('department' in record) { - record['department'] = record['department'].toUpperCase(); + if ('department' in params) { + params.department = params.department.toUpperCase(); } - for (const [key, value] of Object.entries(record)) { + if ('courseNumber' in params) { + params.courseNumber = params.courseNumber.toUpperCase(); + } + for (const [key, value] of Object.entries(params)) { if (value === '') { - delete record[key]; + delete params[key]; } } - return record; + return params; } +/** + * Comparison for two courses based on their course number. + * If the numeric part of their course number is the same, + * returns the lexicographic ordering of their course number. + */ function compareCourses(a: WebsocCourse, b: WebsocCourse) { const aNum = Number.parseInt(a.courseNumber.replaceAll(/\D/g, ''), 10); const bNum = Number.parseInt(b.courseNumber.replaceAll(/\D/g, ''), 10); @@ -48,7 +56,7 @@ function sortWebsocResponse(response: WebsocAPIResponse) { } const queryWebSoc = async ({ input }: { input: Record }) => - await fetch(`https://anteaterapi.com/v2/rest/websoc?${new URLSearchParams(cleanSearchParams(input))}`, { + await fetch(`https://anteaterapi.com/v2/rest/websoc?${new URLSearchParams(sanitizeSearchParams(input))}`, { headers: { ...(process.env.ANTEATER_API_KEY && { Authorization: `Bearer ${process.env.ANTEATER_API_KEY}` }), }, From 7fb801b16dbfceb7be44e7c3847aace0b9be1bb6 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Sun, 17 Nov 2024 16:53:46 -0800 Subject: [PATCH 23/34] docs: PeterPortal => Anteater API --- .../src/components/RightPane/AddedCourses/AddedCoursePane.tsx | 2 +- .../src/components/RightPane/SectionTable/SectionTable.tsx | 2 +- .../components/RightPane/SectionTable/SectionTableBody.tsx | 2 +- apps/antalmanac/src/lib/enrollmentHistory.ts | 4 ++-- apps/antalmanac/src/stores/AppStore.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx b/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx index fa84b8419..7842f4469 100644 --- a/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx +++ b/apps/antalmanac/src/components/RightPane/AddedCourses/AddedCoursePane.tsx @@ -267,7 +267,7 @@ function SkeletonSchedule() { - PeterPortal or WebSoc is currently unreachable. This is the information that we can currently retrieve. + Anteater API is currently unreachable. This is the information that we can currently retrieve. ); diff --git a/apps/antalmanac/src/components/RightPane/SectionTable/SectionTable.tsx b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTable.tsx index 4b34522bf..f2e7c159b 100644 --- a/apps/antalmanac/src/components/RightPane/SectionTable/SectionTable.tsx +++ b/apps/antalmanac/src/components/RightPane/SectionTable/SectionTable.tsx @@ -136,7 +136,7 @@ function SectionTable(props: SectionTableProps) { analyticsCategory={analyticsCategory} /> - {/* Temporarily remove "Past Enrollment" until data on PeterPortal API */} + {/* Temporarily remove "Past Enrollment" until data on Anteater API */} {/* */} { // const { term, sectionCode, courseTitle, courseNumber, status, classes } = props; const { status, classes } = props; - // TODO: Implement course notification when PeterPortal has the functionality, according to #473 + // TODO: Implement course notification when Anteater API has the functionality, according to #473 // if (term === getDefaultTerm().shortName && (status === 'NewOnly' || status === 'FULL')) { // return ( // diff --git a/apps/antalmanac/src/lib/enrollmentHistory.ts b/apps/antalmanac/src/lib/enrollmentHistory.ts index 54c6e7f5c..dd4678e30 100644 --- a/apps/antalmanac/src/lib/enrollmentHistory.ts +++ b/apps/antalmanac/src/lib/enrollmentHistory.ts @@ -73,13 +73,13 @@ export class DepartmentEnrollmentHistory { } /** - * Parses enrollment history data from PeterPortal so that + * Parses enrollment history data from Anteater API so that * we can pass the data into a recharts graph. For each element in the given * array, merge the dates, totalEnrolledHistory, maxCapacityHistory, * and waitlistHistory arrays into one array that contains the enrollment data * for each day. * - * @param res Array of enrollment histories from PeterPortal + * @param res Array of enrollment histories from Anteater API * @returns Array of enrollment histories that we can use for the graph */ static parseEnrollmentHistoryResponse(res: EnrollmentHistoryGraphQL[]): EnrollmentHistory[] { diff --git a/apps/antalmanac/src/stores/AppStore.ts b/apps/antalmanac/src/stores/AppStore.ts index 37dc9c6c7..bfcd43456 100644 --- a/apps/antalmanac/src/stores/AppStore.ts +++ b/apps/antalmanac/src/stores/AppStore.ts @@ -334,7 +334,7 @@ class AppStore extends EventEmitter { this.emit('skeletonModeChange'); - // Switch to added courses tab since PeterPortal can't be reached anyway + // Switch to added courses tab since Anteater API can't be reached anyway useTabStore.getState().setActiveTab(2); } From c1c4529cb66c70d88ce48a930f52b7a242cfb707 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Sun, 17 Nov 2024 17:05:27 -0800 Subject: [PATCH 24/34] fix(search): avoid race condition --- .../CoursePane/SearchForm/FuzzySearch.tsx | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/FuzzySearch.tsx b/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/FuzzySearch.tsx index d55eaac67..b4cd311bc 100644 --- a/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/FuzzySearch.tsx +++ b/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/FuzzySearch.tsx @@ -39,6 +39,7 @@ interface FuzzySearchState { results: Record | undefined; value: string; loading: boolean; + requestTimestamp?: number; pendingRequest?: number; } @@ -49,6 +50,7 @@ class FuzzySearch extends PureComponent { results: {}, value: '', loading: false, + requestTimestamp: undefined, pendingRequest: undefined, }; @@ -124,26 +126,30 @@ class FuzzySearch extends PureComponent { if (this.state.cache[this.state.value]) { this.setState({ results: this.state.cache[this.state.value] }); } else { - this.setState({ results: {}, loading: true }, () => { + const requestTimestamp = Date.now(); + this.setState({ results: {}, loading: true, requestTimestamp }, () => { window.clearTimeout(this.state.pendingRequest); - const pendingRequest = window.setTimeout( - () => - trpc.search.doSearch - .query({ query: this.state.value }) - .then((result) => - this.setState({ - cache: { ...this.state.cache, [this.state.value]: result }, - results: result, - loading: false, - pendingRequest: undefined, - }) - ) - .catch((e) => { - this.setState({ results: {}, loading: false }); - console.error(e); - }), - SEARCH_TIMEOUT_MS - ); + const pendingRequest = window.setTimeout(() => { + if (this.state.requestTimestamp != requestTimestamp) return; + trpc.search.doSearch + .query({ query: this.state.value }) + .then((result) => + this.setState({ + cache: { + ...this.state.cache, + [this.state.value]: result, + }, + results: result, + loading: false, + pendingRequest: undefined, + requestTimestamp: undefined, + }) + ) + .catch((e) => { + this.setState({ results: {}, loading: false }); + console.error(e); + }); + }, SEARCH_TIMEOUT_MS); this.setState({ pendingRequest }); }); } From c50d5319abfe68e462f571cf78e4b9467c5b5271 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Sun, 17 Nov 2024 17:11:40 -0800 Subject: [PATCH 25/34] ci: add api key to backend build steps --- .github/workflows/deploy_production.yml | 3 +++ .github/workflows/deploy_staging.yml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/.github/workflows/deploy_production.yml b/.github/workflows/deploy_production.yml index 322c40567..f05580d43 100644 --- a/.github/workflows/deploy_production.yml +++ b/.github/workflows/deploy_production.yml @@ -113,6 +113,8 @@ jobs: - name: Build backend run: pnpm --filter "antalmanac-backend" build + env: + ANTEATER_API_KEY: ${{ secrets.ANTEATER_API_KEY }} - name: Deploy backend production CloudFormation stack run: pnpm --filter "antalmanac-cdk" backend-production deploy @@ -141,6 +143,7 @@ jobs: run: pnpm --filter "antalmanac-backend" build env: NODE_ENV: development + ANTEATER_API_KEY: ${{ secrets.ANTEATER_API_KEY }} - name: Deploy backend development CloudFormation stack run: pnpm --filter "antalmanac-cdk" backend-development deploy diff --git a/.github/workflows/deploy_staging.yml b/.github/workflows/deploy_staging.yml index 5ca8adcff..4ab39101f 100644 --- a/.github/workflows/deploy_staging.yml +++ b/.github/workflows/deploy_staging.yml @@ -169,6 +169,8 @@ jobs: - name: Build backend run: pnpm --filter "antalmanac-backend" build + env: + ANTEATER_API_KEY: ${{ secrets.ANTEATER_API_KEY }} - name: Deploy backend staging CloudFormation stack run: pnpm --filter "antalmanac-cdk" backend-staging deploy From bce34841588faafc3ac80a8d173c5bf0415a99f4 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Sun, 17 Nov 2024 17:13:56 -0800 Subject: [PATCH 26/34] ci: I'm stupid --- .github/workflows/test.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7b92e550c..ee8c708ce 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -49,6 +49,7 @@ jobs: VITE_TILES_ENDPOINT: ${{ secrets.VITE_TILES_ENDPOINT}} GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} + ANTEATER_API_KEY: ${{ secrets.ANTEATER_API_KEY }} # Turborepo credentials. TURBO_API: ${{ vars.TURBO_API }} From d9d3f4af60c930bd11901dd08abc7c2a10a2f920 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Sun, 17 Nov 2024 17:19:49 -0800 Subject: [PATCH 27/34] fix(backend): i might actually be dumb bruh --- apps/backend/src/routers/websoc.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/routers/websoc.ts b/apps/backend/src/routers/websoc.ts index ac8cdf6ad..28477abac 100644 --- a/apps/backend/src/routers/websoc.ts +++ b/apps/backend/src/routers/websoc.ts @@ -4,13 +4,13 @@ import { procedure, router } from '../trpc'; function sanitizeSearchParams(params: Record) { if ('term' in params) { - const termValue = params.quarter; + const termValue = params.term; const termParts = termValue.split(' '); if (termParts.length === 2) { const [year, quarter] = termParts; delete params.term; params.quarter = quarter; - params.quarter = year; + params.year = year; } } if ('department' in params) { From 9fd4cd442af25916e0386eaa8334d48475f2e807 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Sun, 17 Nov 2024 17:41:39 -0800 Subject: [PATCH 28/34] fix(tour): finals --- apps/antalmanac/src/lib/tourExampleGeneration.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/antalmanac/src/lib/tourExampleGeneration.ts b/apps/antalmanac/src/lib/tourExampleGeneration.ts index fae57f0ff..4d42c8fec 100644 --- a/apps/antalmanac/src/lib/tourExampleGeneration.ts +++ b/apps/antalmanac/src/lib/tourExampleGeneration.ts @@ -1,12 +1,13 @@ import { ScheduleCourse, HourMinute, WebsocSectionFinalExam, WebsocSectionMeeting } from '@packages/antalmanac-types'; -import { daysOfWeek } from '$lib/download'; import AppStore from '$stores/AppStore'; const CURRENT_TERM = '2024 Winter'; // TODO: Check the current term when that PR's in let sampleClassesSectionCodes: Array = []; -type DayOfWeek = (typeof daysOfWeek)[number]; +const finalsDaysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as const; + +type FinalsDaysOfWeek = (typeof finalsDaysOfWeek)[number]; export function addSampleClasses() { if (AppStore.getAddedCourses().length > 0) return; @@ -103,8 +104,8 @@ export function randint(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1)) + min; } -function randomWeekday(): DayOfWeek { - return daysOfWeek[randint(0, 6)]; +function randomWeekdayForFinals(): FinalsDaysOfWeek { + return finalsDaysOfWeek[randint(0, 6)]; } function randomClasstime(): HourMinute { @@ -172,7 +173,7 @@ export function sampleFinalExamFactory({ return { examStatus, - dayOfWeek: dayOfWeek ?? randomWeekday(), + dayOfWeek: dayOfWeek ?? randomWeekdayForFinals(), month, day, startTime: startTime, From d4418f10129885bd6f28abb3f14ee5f5f3f2d8a0 Mon Sep 17 00:00:00 2001 From: MinhxNguyen7 <64875104+MinhxNguyen7@users.noreply.github.com> Date: Sun, 17 Nov 2024 21:45:10 -0800 Subject: [PATCH 29/34] chore: sort order --- apps/backend/src/routers/users.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/routers/users.ts b/apps/backend/src/routers/users.ts index 82d09c5c3..b15c6b9d1 100644 --- a/apps/backend/src/routers/users.ts +++ b/apps/backend/src/routers/users.ts @@ -1,8 +1,8 @@ import { type } from 'arktype'; import { UserSchema } from '@packages/antalmanac-types'; +import { TRPCError } from '@trpc/server'; import { router, procedure } from '../trpc'; import { ddbClient, VISIBILITY } from '../db/ddb'; -import { TRPCError } from '@trpc/server'; const userInputSchema = type([{ userId: 'string' }, '|', { googleId: 'string' }]); From a76fd9a156edb33bde52d86f5f6df25d0943c108 Mon Sep 17 00:00:00 2001 From: MinhxNguyen7 <64875104+MinhxNguyen7@users.noreply.github.com> Date: Sun, 17 Nov 2024 21:54:20 -0800 Subject: [PATCH 30/34] docs: update dotenv and readme --- apps/backend/.env.sample | 6 +++++- apps/backend/README.md | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/backend/.env.sample b/apps/backend/.env.sample index b55f343c7..ef94bb420 100644 --- a/apps/backend/.env.sample +++ b/apps/backend/.env.sample @@ -2,7 +2,7 @@ AA_MONGODB_URI=uri # prod | dev | local -STAGE=dev +STAGE=local # Provided by CDK code when running on AWS. USERDATA_TABLE_NAME=tablename @@ -12,3 +12,7 @@ AWS_REGION=us-east-1 # For Mapbox API MAPBOX_ACCESS_TOKEN=pk.abc123 + +# Anteater API key. +# If you don't have a key, you can get one at https://dashboard.anteaterapi.com +ANTEATER_API_KEY=placeholder diff --git a/apps/backend/README.md b/apps/backend/README.md index 8ca487323..466671113 100644 --- a/apps/backend/README.md +++ b/apps/backend/README.md @@ -18,8 +18,8 @@ The backend should still work, but with limited functionality. Please request credentials from a project lead if you need them. 1. Ensure that you're in the backend project. i.e. `cd apps/backend` from the project root. -1. Change the `.env.sample` to `.env`. -1. Start the server with `pnpm start`. +2. Rename `.env.sample` to `.env` and follow any necessary instructions in there. +3. Start the server with `pnpm start`. ## Privileged From 646ec8508aa06ff7495e9952fdd373742d391f65 Mon Sep 17 00:00:00 2001 From: MinhxNguyen7 <64875104+MinhxNguyen7@users.noreply.github.com> Date: Sun, 17 Nov 2024 21:55:57 -0800 Subject: [PATCH 31/34] chore(deps): remove websoc-fuzzy-search --- apps/antalmanac/package.json | 1 - pnpm-lock.yaml | 16 ---------------- 2 files changed, 17 deletions(-) diff --git a/apps/antalmanac/package.json b/apps/antalmanac/package.json index e6be52aae..7d6a8e0c6 100644 --- a/apps/antalmanac/package.json +++ b/apps/antalmanac/package.json @@ -67,7 +67,6 @@ "recharts": "^2.4.2", "superjson": "^1.12.3", "ua-parser-js": "^1.0.37", - "websoc-fuzzy-search": "^1.0.1", "zustand": "^4.3.2" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f19c8278..cf9439e0b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -197,9 +197,6 @@ importers: ua-parser-js: specifier: ^1.0.37 version: 1.0.37 - websoc-fuzzy-search: - specifier: ^1.0.1 - version: 1.0.1 zustand: specifier: ^4.3.2 version: 4.3.3(react@18.2.0) @@ -4059,9 +4056,6 @@ packages: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} - pako@2.1.0: - resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -4982,9 +4976,6 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} - websoc-fuzzy-search@1.0.1: - resolution: {integrity: sha512-1UlDdT2OvMxVIczNSQzI+vSoojfagbORdwtMQiLAnG1zVLG9Po6x5+VWNysi8w5xoxE2NootQH72HzoenLygDg==} - whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} @@ -9756,8 +9747,6 @@ snapshots: dependencies: aggregate-error: 3.1.0 - pako@2.1.0: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -10686,11 +10675,6 @@ snapshots: webidl-conversions@7.0.0: {} - websoc-fuzzy-search@1.0.1: - dependencies: - base64-arraybuffer: 1.0.2 - pako: 2.1.0 - whatwg-encoding@2.0.0: dependencies: iconv-lite: 0.6.3 From e07eddbc9fc5f058a5bd2555fefc697234fd64e5 Mon Sep 17 00:00:00 2001 From: MinhxNguyen7 <64875104+MinhxNguyen7@users.noreply.github.com> Date: Sun, 17 Nov 2024 21:59:22 -0800 Subject: [PATCH 32/34] chore: add extra console.log to get-search-data --- apps/backend/scripts/get-search-data.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/backend/scripts/get-search-data.ts b/apps/backend/scripts/get-search-data.ts index 457a2082b..a4d438f63 100644 --- a/apps/backend/scripts/get-search-data.ts +++ b/apps/backend/scripts/get-search-data.ts @@ -19,6 +19,8 @@ const ALIASES: Record = { async function main() { const apiKey = process.env.ANTEATER_API_KEY; if (!apiKey) throw new Error("ANTEATER_API_KEY is required"); + console.log("Generating cache for fuzzy search."); + console.log("Fetching courses from Anteater API..."); const headers = { Authorization: `Bearer ${apiKey}` } const courses: Course[] = []; for (let skip = 0; skip < MAX_COURSES; skip += 100) { @@ -26,7 +28,7 @@ async function main() { .then(x => x.json()) .then(x => courses.push(...x.data as Course[])) } - console.log(`Fetched ${courses.length} courses`); + console.log(`Fetched ${courses.length} courses.`); const courseMap = new Map(); const deptMap = new Map(); for (const course of courses) { @@ -47,14 +49,14 @@ async function main() { alias: ALIASES[course.department] }); } - console.log(`Fetched ${deptMap.size} departments`); + console.log(`Fetched ${deptMap.size} departments.`); await mkdir(join(__dirname, "../src/generated/"), { recursive: true }); await writeFile(join(__dirname, "../src/generated/searchData.ts"), ` import type { CourseSearchResult, DepartmentSearchResult } from "@packages/antalmanac-types"; export const departments: Array = ${JSON.stringify(Array.from(deptMap.values()))}; export const courses: Array = ${JSON.stringify(Array.from(courseMap.values()))}; `) - console.log("All done.") + console.log("Cache generated."); } main().then(); From 72144335a5c6ea1ed66f6cbe5007cccd220b934d Mon Sep 17 00:00:00 2001 From: MinhxNguyen7 <64875104+MinhxNguyen7@users.noreply.github.com> Date: Sun, 17 Nov 2024 22:00:43 -0800 Subject: [PATCH 33/34] feat(fuzzy): decrease delay --- .../components/RightPane/CoursePane/SearchForm/FuzzySearch.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/FuzzySearch.tsx b/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/FuzzySearch.tsx index b4cd311bc..eac2258d9 100644 --- a/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/FuzzySearch.tsx +++ b/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/FuzzySearch.tsx @@ -9,7 +9,7 @@ import RightPaneStore from '../../RightPaneStore'; import analyticsEnum, { logAnalytics } from '$lib/analytics'; import trpc from '$lib/api/trpc'; -const SEARCH_TIMEOUT_MS = 300; +const SEARCH_TIMEOUT_MS = 150; const emojiMap: Record = { GE_CATEGORY: '🏫', // U+1F3EB :school: From 7052a3e3ba1bbd3de90440056ca583c2ea488f51 Mon Sep 17 00:00:00 2001 From: MinhxNguyen7 <64875104+MinhxNguyen7@users.noreply.github.com> Date: Sun, 17 Nov 2024 22:17:55 -0800 Subject: [PATCH 34/34] refactor: make fuzzy more readable --- .../CoursePane/SearchForm/FuzzySearch.tsx | 54 +++++++++++-------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/FuzzySearch.tsx b/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/FuzzySearch.tsx index eac2258d9..26e353daa 100644 --- a/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/FuzzySearch.tsx +++ b/apps/antalmanac/src/components/RightPane/CoursePane/SearchForm/FuzzySearch.tsx @@ -113,6 +113,35 @@ class FuzzySearch extends PureComponent { getOptionSelected = () => true; + requestIsCurrent = (requestTimestamp: number) => this.state.requestTimestamp === requestTimestamp; + + // Returns a function for use with setTimeout that exhibits the following behavior: + // If the request is current, make the request. Then, if it is still current, update the component's + // state to reflect the results of the query. + maybeDoSearchFactory = (requestTimestamp: number) => () => { + if (!this.requestIsCurrent(requestTimestamp)) return; + trpc.search.doSearch + .query({ query: this.state.value }) + .then((result) => { + if (!this.requestIsCurrent(requestTimestamp)) return; + this.setState({ + cache: { + ...this.state.cache, + [this.state.value]: result, + }, + results: result, + loading: false, + pendingRequest: undefined, + requestTimestamp: undefined, + }); + }) + .catch((e) => { + if (!this.requestIsCurrent(requestTimestamp)) return; + this.setState({ results: {}, loading: false }); + console.error(e); + }); + }; + onInputChange = (_event: unknown, value: string, reason: AutocompleteInputChangeReason) => { const lowerCaseValue = value.toLowerCase(); if (reason === 'input') { @@ -129,27 +158,10 @@ class FuzzySearch extends PureComponent { const requestTimestamp = Date.now(); this.setState({ results: {}, loading: true, requestTimestamp }, () => { window.clearTimeout(this.state.pendingRequest); - const pendingRequest = window.setTimeout(() => { - if (this.state.requestTimestamp != requestTimestamp) return; - trpc.search.doSearch - .query({ query: this.state.value }) - .then((result) => - this.setState({ - cache: { - ...this.state.cache, - [this.state.value]: result, - }, - results: result, - loading: false, - pendingRequest: undefined, - requestTimestamp: undefined, - }) - ) - .catch((e) => { - this.setState({ results: {}, loading: false }); - console.error(e); - }); - }, SEARCH_TIMEOUT_MS); + const pendingRequest = window.setTimeout( + this.maybeDoSearchFactory(requestTimestamp), + SEARCH_TIMEOUT_MS + ); this.setState({ pendingRequest }); }); }