diff --git a/package-lock.json b/package-lock.json index a8785022d..0d40458d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "@trpc/react-query": "^10.36.0", "@trpc/server": "^10.36.0", "@vercel/analytics": "^0.1.11", + "async-mutex": "^0.4.0", "axios": "^1.3.4", "eslint": "^8.8.0", "eslint-plugin-unused-imports": "^2.0.0", @@ -45,7 +46,6 @@ "prettier-eslint": "^13.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-hotkeys": "^2.0.0", "react-hotkeys-hook": "^4.4.1", "react-loading-skeleton": "^3.3.1", "react-toastify": "^9.1.1", @@ -4827,8 +4827,6 @@ "version": "8.12.0", "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -4843,9 +4841,7 @@ "node_modules/ajv-formats/node_modules/json-schema-traverse": { "version": "1.0.0", "dev": true, - "license": "MIT", - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/ajv-keywords": { "version": "3.5.2", @@ -5139,6 +5135,14 @@ "dev": true, "license": "MIT" }, + "node_modules/async-mutex": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.0.tgz", + "integrity": "sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "license": "MIT" @@ -13856,17 +13860,6 @@ "react": ">=16.0.0" } }, - "node_modules/react-hotkeys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/react-hotkeys/-/react-hotkeys-2.0.0.tgz", - "integrity": "sha512-3n3OU8vLX/pfcJrR3xJ1zlww6KS1kEJt0Whxc4FiGV+MJrQ1mYSYI3qS/11d2MJDFm8IhOXMTFQirfu6AVOF6Q==", - "dependencies": { - "prop-types": "^15.6.1" - }, - "peerDependencies": { - "react": ">= 0.14.0" - } - }, "node_modules/react-hotkeys-hook": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.4.1.tgz", @@ -20329,13 +20322,13 @@ "ajv-formats": { "version": "2.1.1", "dev": true, - "requires": {}, + "requires": { + "ajv": "^8.0.0" + }, "dependencies": { "ajv": { "version": "8.12.0", "dev": true, - "optional": true, - "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -20345,9 +20338,7 @@ }, "json-schema-traverse": { "version": "1.0.0", - "dev": true, - "optional": true, - "peer": true + "dev": true } } }, @@ -20522,6 +20513,14 @@ "version": "3.2.4", "dev": true }, + "async-mutex": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.0.tgz", + "integrity": "sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==", + "requires": { + "tslib": "^2.4.0" + } + }, "asynckit": { "version": "0.4.0" }, @@ -26109,14 +26108,6 @@ "prop-types": "^15.7.2" } }, - "react-hotkeys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/react-hotkeys/-/react-hotkeys-2.0.0.tgz", - "integrity": "sha512-3n3OU8vLX/pfcJrR3xJ1zlww6KS1kEJt0Whxc4FiGV+MJrQ1mYSYI3qS/11d2MJDFm8IhOXMTFQirfu6AVOF6Q==", - "requires": { - "prop-types": "^15.6.1" - } - }, "react-hotkeys-hook": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.4.1.tgz", @@ -27201,6 +27192,7 @@ "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", + "postcss": "^8.4.18", "postcss-import": "^14.1.0", "postcss-js": "^4.0.0", "postcss-load-config": "^3.1.4", diff --git a/package.json b/package.json index 54c75564e..004b78a8d 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { + "bootstrap": "npm run prisma:generate && npx prisma migrate dev && npm run dev", "dev": "next dev", "prisma:generate": "prisma generate --schema prisma/platform.prisma && prisma generate --schema prisma/schema.prisma", "prisma:migrate:deploy": "prisma migrate deploy --schema prisma/schema.prisma", @@ -44,6 +45,7 @@ "@trpc/react-query": "^10.36.0", "@trpc/server": "^10.36.0", "@vercel/analytics": "^0.1.11", + "async-mutex": "^0.4.0", "axios": "^1.3.4", "eslint": "^8.8.0", "eslint-plugin-unused-imports": "^2.0.0", diff --git a/prisma/platform.prisma b/prisma/platform.prisma index 728910b8a..f6f14602b 100644 --- a/prisma/platform.prisma +++ b/prisma/platform.prisma @@ -8,6 +8,223 @@ datasource db { url = env("PLATFORM_DATABASE_URL") } +type CoursesCoOrPreRequisites { + name String + options CoursesCoOrPreRequisitesOptions[] + required Int + type String +} + +type CoursesCoOrPreRequisitesOptions { + choices CoursesCoOrPreRequisitesOptionsChoices? + class_reference String? + condition String? + description String? + max_hours Int? + minimum_grade String? + name String? + options CoursesCoOrPreRequisitesOptionsOptions[] + required Int? + type String +} + +type CoursesCoOrPreRequisitesOptionsChoices { + name String + options CoursesCoOrPreRequisitesOptionsChoicesOptions[] + required Int + type String +} + +type CoursesCoOrPreRequisitesOptionsChoicesOptions { + class_reference String? + condition String? + description String? + minimum_grade String? + type String +} + +type CoursesCoOrPreRequisitesOptionsOptions { + class_reference String? + condition String? + description String? + minimum_grade String? + name String? + options CoursesCoOrPreRequisitesOptionsOptionsOptions[] + required Int? + type String +} + +type CoursesCoOrPreRequisitesOptionsOptionsOptions { + class_reference String? + condition String? + description String? + major String? + minimum_grade String? + minor String? + type String +} + +type CoursesCorequisites { + name String + options CoursesCorequisitesOptions[] + required Int + type String +} + +type CoursesCorequisitesOptions { + choices CoursesCorequisitesOptionsChoices? + class_reference String? + condition String? + description String? + max_hours Int? + minimum_grade String? + name String? + options CoursesCorequisitesOptionsOptions[] + required Int? + type String +} + +type CoursesCorequisitesOptionsChoices { + name String + options CoursesCorequisitesOptionsChoicesOptions[] + required Int + type String +} + +type CoursesCorequisitesOptionsChoicesOptions { + class_reference String? + condition String? + description String? + minimum_grade String? + name String? + options CoursesCorequisitesOptionsChoicesOptionsOptions[] + required Int? + type String +} + +type CoursesCorequisitesOptionsChoicesOptionsOptions { + class_reference String + minimum_grade String + type String +} + +type CoursesCorequisitesOptionsOptions { + class_reference String? + condition String? + description String? + minimum_grade String? + name String? + options CoursesCorequisitesOptionsOptionsOptions[] + required Int? + type String +} + +type CoursesCorequisitesOptionsOptionsOptions { + class_reference String? + condition String? + description String? + major String? + minimum_grade String? + minor String? + type String +} + +type CoursesPrerequisites { + name String + options CoursesPrerequisitesOptions[] + required Int + type String +} + +type CoursesPrerequisitesOptions { + choices CoursesPrerequisitesOptionsChoices? + class_reference String? + condition String? + core_flag String? + description String? + hours Int? + major String? + max_hours Int? + minimum Float? + minimum_grade String? + name String? + options CoursesPrerequisitesOptionsOptions[] + required Int? + subset String? + type String +} + +type CoursesPrerequisitesOptionsChoices { + name String + options CoursesPrerequisitesOptionsChoicesOptions[] + required Int + type String +} + +type CoursesPrerequisitesOptionsChoicesOptions { + class_reference String? + condition String? + description String? + minimum_grade String? + name String? + options CoursesPrerequisitesOptionsChoicesOptionsOptions[] + required Int? + type String +} + +type CoursesPrerequisitesOptionsChoicesOptionsOptions { + class_reference String + minimum_grade String + type String +} + +type CoursesPrerequisitesOptionsOptions { + class_reference String? + condition String? + core_flag String? + description String? + granter String? + hours Int? + major String? + minimum_grade String? + name String? + options CoursesPrerequisitesOptionsOptionsOptions[] + required Int? + type String +} + +type CoursesPrerequisitesOptionsOptionsOptions { + class_reference String? + condition String? + description String? + granter String? + major String? + minimum_grade String? + name String? + options CoursesPrerequisitesOptionsOptionsOptionsOptions[] + required Int? + type String +} + +type CoursesPrerequisitesOptionsOptionsOptionsOptions { + class_reference String? + condition String? + description String? + minimum_grade String? + name String? + options CoursesPrerequisitesOptionsOptionsOptionsOptionsOptions[] + required Int? + type String +} + +type CoursesPrerequisitesOptionsOptionsOptionsOptionsOptions { + class_reference String? + condition String? + description String? + minimum_grade String? + type String +} + type ExamsYields { outcome Json requirement ExamsYieldsRequirement @@ -66,31 +283,20 @@ type SectionsAcademicSession { start_date String } -type SectionsAttributes { - raw_attributes String[] -} - type SectionsMeetings { - end_date String? + end_date String end_time String location SectionsMeetingsLocation meeting_days String[] - /// Could not determine type: the field only had null or empty values in the sample set. - modality Json? + modality String start_date String start_time String } type SectionsMeetingsLocation { - building String? + building String map_uri String - room String? -} - -type SectionsSectionCorequisites { - /// Could not determine type: the field only had null or empty values in the sample set. - options Json? - type String + room String } type SectionsTeachingAssistants { @@ -101,64 +307,80 @@ type SectionsTeachingAssistants { } model courses { - id String @id @default(auto()) @map("_id") @db.ObjectId - v Int @map("__v") + id String @id @default(auto()) @map("_id") @db.ObjectId activity_type String + /// Could not determine type: the field only had null or empty values in the sample set. + attributes Json? + catalog_year String class_level String - co_or_pre_requisites Json - corequisites Json + co_or_pre_requisites CoursesCoOrPreRequisites? + corequisites CoursesCorequisites? course_number String credit_hours String description String + enrollment_reqs String grading String internal_course_number String - laboratory_contact_hours String? - lecture_contact_hours String? - offering_frequency String? - prerequisites Json + laboratory_contact_hours String + lecture_contact_hours String + offering_frequency String + prerequisites CoursesPrerequisites? school String - sections String[] @db.ObjectId + sections String[] subject_prefix String title String } +model degrees { + id String @id @default(auto()) @map("_id") @db.ObjectId +} + model exams { id String @id @default(auto()) @map("_id") @db.ObjectId + level String? name String? type String yields ExamsYields[] + + @@index([type], map: "type_1") + @@index([name], map: "name_1") + @@index([level], map: "level_1") } model professors { - id String @id @default(auto()) @map("_id") @db.ObjectId - v Int @map("__v") + id String @id @default(auto()) @map("_id") @db.ObjectId email String first_name String - image_uri String? + image_uri String last_name String - office ProfessorsOffice? + office ProfessorsOffice /// Could not determine type: the field only had null or empty values in the sample set. office_hours Json? - phone_number String? - profile_uri String? - sections String[] @db.ObjectId + phone_number String + profile_uri String + sections String[] titles String[] + + @@index([first_name, last_name], map: "first_name_1_last_name_1") } model sections { id String @id @default(auto()) @map("_id") @db.ObjectId - v Int @map("__v") academic_session SectionsAcademicSession - attributes SectionsAttributes + /// Could not determine type: the field only had null or empty values in the sample set. + attributes Json? core_flags String[] course_reference String @db.ObjectId grade_distribution Int[] instruction_mode String internal_class_number String meetings SectionsMeetings[] - professors String[] @db.ObjectId - section_corequisites SectionsSectionCorequisites + professors String[] + /// Could not determine type: the field only had null or empty values in the sample set. + section_corequisites Json? section_number String - syllabus_uri String? + syllabus_uri String teaching_assistants SectionsTeachingAssistants[] + + @@index([course_reference], map: "course_reference_1") } diff --git a/src/components/planner/Sidebar/RequirementsContainer.tsx b/src/components/planner/Sidebar/RequirementsContainer.tsx index 93741be76..b1cb2603e 100644 --- a/src/components/planner/Sidebar/RequirementsContainer.tsx +++ b/src/components/planner/Sidebar/RequirementsContainer.tsx @@ -1,10 +1,10 @@ import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import * as HoverCard from '@radix-ui/react-hover-card'; -import React, { useState } from 'react'; +import { useState, useEffect } from 'react'; import useSearch from '@/components/search/search'; -import { JSONCourse } from '@/data/courses.json'; import { trpc } from '@/utils/trpc'; +import { courses as Course } from 'prisma/generated/platform'; import Accordion from './Accordion'; import { RecursiveRequirement } from './RecursiveRequirement'; @@ -16,7 +16,6 @@ import { CourseRequirement, DegreeRequirement, } from './types'; -import { useSemestersContext } from '../SemesterContext'; import { GetDragIdByCourseAndReq } from '../types'; function RequirementContainerHeader({ @@ -69,7 +68,7 @@ function RequirementContainerHeader({ - + ); @@ -77,7 +76,7 @@ function RequirementContainerHeader({ const getRequirementGroup = ( degreeRequirement: RequirementGroupTypes, - allCourses: JSONCourse[] | undefined, + allCourses: Course[] | undefined, ): { name: string; progress: { value: number; max: number; unit: string }; @@ -200,15 +199,7 @@ const getRequirementGroup = ( } }; -export const ProgressComponent2 = ({ - value, - max, - unit = 'done', -}: { - value: number; - max: number; - unit?: string; -}) => { +export const ProgressComponent2 = ({ value, max }: { value: number; max: number }) => { const heh = `${(value * 100) / max}%`; return ( @@ -269,7 +260,7 @@ export default function RequirementsContainer({ courses, getCourseItemDragId, }: RequirementsContainerProps) { - const [requirementIdx, setRequirementIdx] = React.useState(0); + const [requirementIdx, setRequirementIdx] = useState(0); const q = trpc.courses.publicGetAllCourses.useQuery(undefined, { staleTime: Infinity, @@ -280,7 +271,7 @@ export default function RequirementsContainer({ /** * These hooks manage the carousel state for RequirementsCarousel */ - const [carousel, setCarousel] = React.useState(false); + const [carousel, setCarousel] = useState(false); // Note: this logic hides overflow during sliding animation const [overflow, setOverflow] = useState(false); @@ -290,8 +281,6 @@ export default function RequirementsContainer({ setCarousel(!carousel); } - const { bypasses } = useSemestersContext(); - return ( { + useEffect(() => { updateQuery(''); - }, [degreeRequirement]); + }, [degreeRequirement]); // eslint-disable-line react-hooks/exhaustive-deps // Put filled requirements first const sortedResults = [...results] @@ -410,18 +393,8 @@ function RequirementContainer({ return 0; }) .slice(0, 100); - - const { planId, bypasses, handleAddBypass, handleRemoveBypass } = useSemestersContext(); - - const hasBypass = bypasses.includes(degreeRequirement.metadata.id.toString()); - - const handleUpdateBypass = () => { - hasBypass - ? handleRemoveBypass({ planId, requirement: degreeRequirement.metadata.id.toString() }) - : handleAddBypass({ planId, requirement: degreeRequirement.metadata.id.toString() }); - }; - // Handles logic for adding bypass to requirement + return ( <> @@ -442,9 +415,6 @@ function RequirementContainer({ ); })} - {/* */} ); } diff --git a/src/components/planner/useGetCourseInfo.ts b/src/components/planner/useGetCourseInfo.ts index da700cfc3..135339324 100644 --- a/src/components/planner/useGetCourseInfo.ts +++ b/src/components/planner/useGetCourseInfo.ts @@ -52,11 +52,11 @@ const getPrereqs = ( if (cNum.subject_prefix + ' ' + cNum.course_number === courseCode) { title = cNum.title; - (cNum.prerequisites as Record).options.map((elem: any) => { + cNum.prerequisites?.options.map((elem) => { if (elem.type !== 'course' && elem.type !== 'other' && elem.options) { - elem.options.map((elem2: any) => { + elem.options.map((elem2) => { if (elem2.type !== 'course' && elem2.type !== 'other') { - elem2.options?.map((elem3: any) => { + elem2.options?.map((elem3) => { courseData?.map((elem4) => { if (elem4.id === elem3.class_reference) { prereqs.push(elem4.subject_prefix + ' ' + elem4.course_number); @@ -64,7 +64,7 @@ const getPrereqs = ( }); }); } else if (elem2.type === 'other') { - prereqs.push(elem2.description); + prereqs.push(elem2.description || ''); } else { courseData?.map((elem4) => { if (elem4.id === elem2.class_reference) { @@ -74,7 +74,7 @@ const getPrereqs = ( } }); } else if (elem.type === 'other') { - prereqs.push(elem.description); + prereqs.push(elem.description || ''); } else { courseData?.map((elem4) => { if (elem4.id === elem.class_reference) { @@ -83,11 +83,11 @@ const getPrereqs = ( }); } }); - (cNum.corequisites as Record).options.map((elem: any) => { + cNum.corequisites?.options.map((elem) => { if (elem.type !== 'course' && elem.type !== 'other' && elem.options) { - elem.options.map((elem2: any) => { + elem.options.map((elem2) => { if (elem2.type !== 'course' && elem2.type !== 'other') { - elem2.options?.map((elem3: any) => { + elem2.options?.map((elem3) => { courseData?.map((elem4) => { if (elem4.id === elem3.class_reference) { coreqs.push(elem4.subject_prefix + ' ' + elem4.course_number); @@ -95,7 +95,7 @@ const getPrereqs = ( }); }); } else if (elem2.type === 'other') { - coreqs.push(elem2.description); + coreqs.push(elem2.description || ''); } else { courseData?.map((elem4) => { if (elem4.id === elem2.class_reference) { @@ -105,7 +105,7 @@ const getPrereqs = ( } }); } else if (elem.type === 'other') { - coreqs.push(elem.description); + coreqs.push(elem.description || ''); } else { courseData?.map((elem4) => { if (elem4.id === elem.class_reference) { @@ -114,11 +114,11 @@ const getPrereqs = ( }); } }); - (cNum.co_or_pre_requisites as Record).options.map((elem: any) => { + cNum.co_or_pre_requisites?.options.map((elem) => { if (elem.type !== 'course' && elem.type !== 'other' && elem.options) { - elem.options.map((elem2: any) => { + elem.options.map((elem2) => { if (elem2.type !== 'course' && elem2.type !== 'other') { - elem2.options?.map((elem3: any) => { + elem2.options?.map((elem3) => { courseData?.map((elem4) => { if (elem4.id === elem3.class_reference) { co_or_pre.push(elem4.subject_prefix + ' ' + elem4.course_number); @@ -126,7 +126,7 @@ const getPrereqs = ( }); }); } else if (elem2.type === 'other') { - co_or_pre.push(elem2.description); + co_or_pre.push(elem2.description || ''); } else { courseData?.map((elem4) => { if (elem4.id === elem2.class_reference) { @@ -136,7 +136,7 @@ const getPrereqs = ( } }); } else if (elem.type === 'other') { - co_or_pre.push(elem.description); + co_or_pre.push(elem.description || ''); } else { courseData?.map((elem4) => { if (elem4.id === elem.class_reference) { diff --git a/src/server/trpc/router/courseCache.ts b/src/server/trpc/router/courseCache.ts new file mode 100644 index 000000000..26100155e --- /dev/null +++ b/src/server/trpc/router/courseCache.ts @@ -0,0 +1,46 @@ +import { Mutex } from 'async-mutex'; + +import { platformPrisma } from '@/server/db/platform_client'; +import { courses as Course } from 'prisma/generated/platform'; + +class CourseCacheError extends Error { + name = 'CourseCacheError'; +} + +class CourseCache { + private coursesByYear: Map = new Map(); + private mutex = new Mutex(); + + public async getCourses(year: number) { + const formattedYear = year.toString().slice(-2); + // Acquire lock before success check so if another request is fetching, we don't fetch again. + const release = await this.mutex.acquire(); + if (this.coursesByYear.has(year)) { + release(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.coursesByYear.get(year)!; // Must exist at this point + } + + console.info(`Fetching courses for year ${year}...`); + return await platformPrisma.courses + .findMany({ + where: { + catalog_year: formattedYear, + }, + }) + .then((courses) => { + this.coursesByYear.set(year, courses); + return courses; + }) + .catch((err) => { + const message = `Error fetching courses for year ${year}: ${err}`; + console.error(message); + throw new CourseCacheError(message, { cause: err }); + }) + .finally(release); + } +} + +const courseCache = new CourseCache(); + +export { courseCache }; diff --git a/src/server/trpc/router/courses.ts b/src/server/trpc/router/courses.ts index 1305e6542..d3af6eb85 100644 --- a/src/server/trpc/router/courses.ts +++ b/src/server/trpc/router/courses.ts @@ -1,12 +1,11 @@ import { Prisma } from '@prisma/client'; -import courses, { JSONCourse } from '@data/courses.json'; - +import { courseCache } from './courseCache'; import { router, publicProcedure } from '../trpc'; export const coursesRouter = router({ - publicGetAllCourses: publicProcedure.query(async ({ ctx }) => { - return courses as JSONCourse[]; + publicGetAllCourses: publicProcedure.query(async () => { + return await courseCache.getCourses(new Date().getFullYear()); }), publicGetSanitizedCourses: publicProcedure.query(async ({ ctx }) => { const courses = await ctx.platformPrisma.courses.findMany({ diff --git a/src/server/trpc/router/validator.ts b/src/server/trpc/router/validator.ts index 3a2196645..77b49326b 100644 --- a/src/server/trpc/router/validator.ts +++ b/src/server/trpc/router/validator.ts @@ -3,8 +3,9 @@ import { TRPCError } from '@trpc/server'; import { z } from 'zod'; import { env } from '@/env/server.mjs'; -import courses, { JSONCourse } from '@data/courses.json'; +import { courses as PlatformCourse } from 'prisma/generated/platform'; +import { courseCache } from './courseCache'; import { DegreeNotFound, DegreeValidationError } from './errors'; import { protectedProcedure, router } from '../trpc'; @@ -47,7 +48,13 @@ export const validatorRouter = router({ throw new TRPCError({ code: 'FORBIDDEN' }); } - const coursesFromApi: JSONCourse[] = courses; + let year = new Date().getFullYear(); // If plan has no semesters, default to current year. + if (planData.semesters.length > 0) { + // If plan has semesters, default to first semester's year. + year = Math.min(...planData.semesters.map((sem) => sem.year)); + } + + const coursesFromAPI: PlatformCourse[] = await courseCache.getCourses(year); /* sanitizing data from API db. * TODO: Fix this later somehow */ @@ -61,13 +68,16 @@ export const validatorRouter = router({ } >(); - for (const course of coursesFromApi) { + for (const course of coursesFromAPI) { courseMapWithCodeKey.set(`${course.subject_prefix} ${course.course_number}`, { prereqs: course.prerequisites, coreqs: course.corequisites, co_or_pre_requisites: course.co_or_pre_requisites, }); - courseMapWithIdKey.set(course.id, `${course.subject_prefix} ${course.course_number}`); + courseMapWithIdKey.set( + course.internal_course_number, + `${course.subject_prefix} ${course.course_number}`, + ); } /* Hash to store pre req data. @@ -104,7 +114,7 @@ export const validatorRouter = router({ ): Array<[Array, number]> => { const prereqNotMet: Array<[Array, number]> = []; let count = 0; - if (requirements.options.length === 0) { + if (!requirements || requirements.options.length === 0) { return []; } const temp: [Array, number] = [[], 0]; @@ -152,7 +162,7 @@ export const validatorRouter = router({ ): Array<[Array, number]> => { const coreqNotMet: Array<[Array, number]> = []; let count = 0; - if (requirements.options.length === 0) { + if (!requirements || requirements.options.length === 0) { return []; } const temp: [Array, number] = [[], 0]; @@ -201,7 +211,7 @@ export const validatorRouter = router({ ): Array<[Array, number]> => { const coreqNotMet: Array<[Array, number]> = []; let count = 0; - if (requirements.options.length === 0) { + if (!requirements || requirements.options.length === 0) { return []; } const temp: [Array, number] = [[], 0];